From d277e080567604a605b495c11da5610990b636d3 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Tue, 16 Dec 2025 21:16:50 -0800 Subject: [PATCH] refactoring --- .agent/rules/indicator.md | 232 ++++ docs/API.md | 213 +++ lib/_index.md | 4 +- lib/core/AbstractBase.cs | 69 + lib/core/simd/SimdExtensions.Tests.cs | 26 + lib/core/simd/SimdExtensions.cs | 1424 ++++++++++---------- lib/core/tbar/tbar.cs | 166 +-- lib/core/tbarseries/tbarseries.cs | 294 ++-- lib/core/tseries/tseries.cs | 268 ++-- lib/core/tvalue/tvalue.cs | 94 +- lib/momentum/adx/Adx.Quantower.Tests.cs | 6 +- lib/momentum/adx/Adx.Quantower.cs | 6 +- lib/momentum/adx/Adx.Tests.cs | 2 +- lib/momentum/adx/Adx.cs | 32 +- lib/momentum/adx/Adx.md | 4 +- lib/momentum/ao/Ao.Quantower.Tests.cs | 8 +- lib/momentum/ao/Ao.Quantower.cs | 2 +- lib/momentum/ao/Ao.Tests.cs | 4 +- lib/momentum/ao/Ao.cs | 8 +- lib/momentum/ao/Ao.md | 4 + lib/momentum/cfb/Cfb.Quantower.Tests.cs | 6 +- lib/momentum/cfb/Cfb.Tests.cs | 8 +- lib/momentum/cfb/Cfb.Validation.Tests.cs | 16 +- lib/momentum/cfb/Cfb.cs | 38 +- lib/momentum/cfb/Cfb.md | 2 +- lib/momentum/dmx/Dmx.Quantower.Tests.cs | 4 +- lib/momentum/dmx/Dmx.Tests.cs | 4 +- lib/momentum/dmx/Dmx.cs | 10 +- lib/momentum/dmx/Dmx.md | 3 +- lib/momentum/rsx/Rsx.Quantower.Tests.cs | 2 +- lib/momentum/rsx/Rsx.Tests.cs | 8 +- lib/momentum/rsx/Rsx.cs | 16 +- lib/momentum/rsx/Rsx.md | 2 +- lib/momentum/vel/Vel.Quantower.Tests.cs | 2 +- lib/momentum/vel/Vel.Tests.cs | 10 +- lib/momentum/vel/Vel.cs | 12 +- lib/momentum/vel/Vel.md | 4 +- lib/trends/_index.md | 2 +- lib/trends/alma/Alma.Tests.cs | 6 +- lib/trends/alma/Alma.cs | 72 +- lib/trends/alma/Alma.md | 2 +- lib/trends/conv/Conv.Quantower.Tests.cs | 6 +- lib/trends/conv/Conv.Quantower.cs | 2 +- lib/trends/conv/Conv.Tests.cs | 12 +- lib/trends/conv/Conv.cs | 44 +- lib/trends/conv/Conv.md | 2 +- lib/trends/dema/Dema.Quantower.Tests.cs | 2 +- lib/trends/dema/Dema.Tests.cs | 6 +- lib/trends/dema/Dema.cs | 34 +- lib/trends/dema/Dema.md | 6 +- lib/trends/dwma/Dwma.Tests.cs | 4 +- lib/trends/dwma/Dwma.cs | 55 +- lib/trends/dwma/Dwma.md | 2 +- lib/trends/ema/Ema.Quantower.cs | 130 +- lib/trends/ema/Ema.Tests.cs | 127 +- lib/trends/ema/Ema.Validation.Tests.cs | 6 +- lib/trends/ema/Ema.cs | 690 ++++++---- lib/trends/ema/Ema.md | 10 +- lib/trends/hma/Hma.Quantower.Tests.cs | 2 +- lib/trends/hma/Hma.Tests.cs | 6 +- lib/trends/hma/Hma.cs | 63 +- lib/trends/hma/Hma.md | 37 + lib/trends/htit/Htit.Quantower.Tests.cs | 2 +- lib/trends/htit/Htit.Quantower.cs | 2 +- lib/trends/htit/Htit.Tests.cs | 2 +- lib/trends/htit/Htit.cs | 44 +- lib/trends/htit/Htit.md | 4 +- lib/trends/jma/Jma.Quantower.Tests.cs | 2 +- lib/trends/jma/Jma.Tests.cs | 10 +- lib/trends/jma/Jma.cs | 81 +- lib/trends/jma/Jma.md | 2 +- lib/trends/kama/Kama.Tests.cs | 2 +- lib/trends/kama/Kama.cs | 75 +- lib/trends/kama/Kama.md | 2 +- lib/trends/lsma/Lsma.Quantower.Tests.cs | 2 +- lib/trends/lsma/Lsma.Tests.cs | 2 +- lib/trends/lsma/Lsma.cs | 75 +- lib/trends/lsma/Lsma.md | 2 +- lib/trends/mama/Mama.Quantower.Tests.cs | 2 +- lib/trends/mama/Mama.Quantower.cs | 10 +- lib/trends/mama/Mama.Tests.cs | 2 +- lib/trends/mama/Mama.cs | 207 ++- lib/trends/mama/Mama.md | 38 + lib/trends/mgdi/Mgdi.Quantower.Tests.cs | 2 +- lib/trends/mgdi/Mgdi.Quantower.cs | 2 +- lib/trends/mgdi/Mgdi.Tests.cs | 2 +- lib/trends/mgdi/Mgdi.cs | 47 +- lib/trends/mgdi/Mgdi.md | 2 +- lib/trends/pwma/Pwma.Quantower.Tests.cs | 2 +- lib/trends/pwma/Pwma.Tests.cs | 9 +- lib/trends/pwma/Pwma.cs | 38 +- lib/trends/pwma/Pwma.md | 6 +- lib/trends/rma/Rma.Tests.cs | 4 +- lib/trends/rma/Rma.cs | 78 +- lib/trends/rma/Rma.md | 2 +- lib/trends/sma/Sma.Quantower.Tests.cs | 2 +- lib/trends/sma/Sma.Quantower.cs | 6 +- lib/trends/sma/Sma.Tests.cs | 116 +- lib/trends/sma/Sma.Validation.Tests.cs | 6 +- lib/trends/sma/Sma.cs | 176 ++- lib/trends/sma/Sma.md | 6 +- lib/trends/super/Super.Quantower.Tests.cs | 8 +- lib/trends/super/Super.Quantower.cs | 6 +- lib/trends/super/Super.Tests.cs | 4 +- lib/trends/super/Super.cs | 6 +- lib/trends/super/Super.md | 8 + lib/trends/t3/T3.Tests.cs | 16 +- lib/trends/t3/T3.Validation.Tests.cs | 2 +- lib/trends/t3/T3.cs | 98 +- lib/trends/t3/T3.md | 4 +- lib/trends/tema/Tema.Quantower.Tests.cs | 2 +- lib/trends/tema/Tema.Tests.cs | 16 +- lib/trends/tema/Tema.Validation.Tests.cs | 2 +- lib/trends/tema/Tema.cs | 118 +- lib/trends/tema/Tema.md | 6 +- lib/trends/trima/Trima.Quantower.Tests.cs | 2 +- lib/trends/trima/Trima.Tests.cs | 14 +- lib/trends/trima/Trima.Validation.Tests.cs | 2 +- lib/trends/trima/Trima.cs | 192 +-- lib/trends/trima/Trima.md | 23 + lib/trends/vidya/Vidya.Tests.cs | 14 +- lib/trends/vidya/Vidya.Validation.Tests.cs | 16 + lib/trends/vidya/Vidya.cs | 252 ++-- lib/trends/vidya/Vidya.md | 2 +- lib/trends/wma/Wma.Quantower.Tests.cs | 2 +- lib/trends/wma/Wma.Tests.cs | 8 +- lib/trends/wma/Wma.Validation.Tests.cs | 6 +- lib/trends/wma/Wma.cs | 127 +- lib/trends/wma/Wma.md | 6 +- lib/volatility/atr/Atr.Tests.cs | 84 ++ lib/volatility/atr/Atr.Validation.Tests.cs | 67 + lib/volatility/atr/Atr.cs | 199 +++ quantower/IndicatorExtensions.Tests.cs | 3 + quantower/IndicatorExtensions.cs | 733 ++++++---- quantower/Mocks/TradingPlatformMocks.cs | 779 ++++++----- quantower/Quantower.Tests.csproj | 6 +- quantower/Trends.csproj | 4 +- 137 files changed, 5074 insertions(+), 3178 deletions(-) create mode 100644 .agent/rules/indicator.md create mode 100644 docs/API.md create mode 100644 lib/core/AbstractBase.cs create mode 100644 lib/volatility/atr/Atr.Tests.cs create mode 100644 lib/volatility/atr/Atr.Validation.Tests.cs create mode 100644 lib/volatility/atr/Atr.cs diff --git a/.agent/rules/indicator.md b/.agent/rules/indicator.md new file mode 100644 index 00000000..3f8c611b --- /dev/null +++ b/.agent/rules/indicator.md @@ -0,0 +1,232 @@ +--- +trigger: always_on +--- + +# QuanTAlib Protocol + +> **To all AI Agents:** This file defines the laws, physics, and protocols of the QuanTAlib repository. Read this before writing a single line of code. Failure to adhere to these standards will result in rejected code. + +## 1. Identity & Mission + +**QuanTAlib** is a high-performance, zero-allocation C# library for quantitative technical analysis. + +* **Target**: Quantower and custom C# trading engines. +* **Core Philosophy**: Speed, Correctness, and Memory Efficiency. +* **Key Constraint**: Hot paths must be allocation-free (GC pressure is the enemy). + +## 2. Architecture & "Physics" + +### Memory Model: Structure of Arrays (SoA) + +We do not store objects in lists. We store primitive arrays. + +* **TSeries**: Internally uses `List _t` (timestamps) and `List _v` (values). +* **Access**: Expose data via `ReadOnlySpan` for SIMD operations. + +### Core Types + +* `TValue`: Struct (16 bytes). `DateTime Time`, `double Value`. +* `TBar`: Struct (48 bytes). `DateTime Time`, `double Open, High, Low, Close, Volume`. +* `TSeries`: The primary data structure for time series. +* `ITValuePublisher`: The interface for reactive data flow. + +### Design Principles + +* **Source Material:** The algorithm and markdown documentation foundation should be sourced from [https://github.com/mihakralj/pinescript/blob/main/indicators/](PineScript). +* **Zero Allocation:** The core calculation loop must not allocate memory on the heap. Use `stackalloc`, `Span`, and pinned memory where possible. +* **O(1) Complexity:** Streaming updates must be O(1) whenever mathematically possible. Use running sums/products or circular buffers to avoid re-iterating over history. +* **Dual API:** Provide both a stateful object-oriented API (`Update`) and a stateless static vector API (`Calculate`). +* **Bar Correction:** Support intra-bar updates via the `isNew` parameter. The indicator must be able to rollback the last update and apply a new value for the same timestamp. +* **Robustness:** Handle `NaN` and `Infinity` gracefully using last-valid-value substitution. Never propagate invalid values. +* **Reactive:** Implement `ITValuePublisher` to support event-driven architectures. +* **Time Handling:** Always use `DateTime.UtcNow` instead of `DateTime.Now` to ensure consistent time handling across timezones. + +### Performance Rules + +1. **Zero Allocation**: The `Update` method MUST NOT allocate memory on the heap. Use `stackalloc` or pre-allocated buffers. +2. **O(1) Complexity**: Streaming updates must be constant time. Use circular buffers (`RingBuffer`) or running sums. +3. **SIMD**: Batch operations (`Calculate`) should use `System.Runtime.Intrinsics` (AVX2) where possible. If SIMD is not possible due to recursive dependencies, use `stackalloc` for internal buffers to avoid heap allocations. +4. **Inlining**: Use `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on hot methods. +5. **Locals**: Use `[SkipLocalsInit]` to avoid zero-init costs in tight loops. + +## 3. Indicator Implementation Standards + +Every indicator must follow the **Good Indicator Guidelines** strictly. + +### File Structure + +Directory: `lib/[category]/[name]/` (e.g., `lib/trends/sma/`) + +| File | Naming | Purpose | +|------|--------|---------| +| **Source** | `[Name].cs` | Main implementation. `public sealed class`. | +| **Tests** | `[Name].Tests.cs` | xUnit tests (correctness, edge cases). | +| **Validation** | `[Name].Validation.Tests.cs` | Compare against TA-Lib, Skender, etc. | +| **Docs** | `[Name].md` | User documentation with formulas. | +| **Adapter** | `[Name].Quantower.cs` | Quantower platform integration. | +| **Adapter Tests** | `[Name].Quantower.Tests.cs` | Tests for the adapter. | + +### Class Definition + +* **Namespace:** `QuanTAlib` +* **Attributes:** `[SkipLocalsInit]` for performance. +* **Modifiers:** `public sealed class` +* **Interface:** Implements `ITValuePublisher` + +### State Management + +* **Scalar State:** Use a `private record struct State` to group all scalar state variables. This ensures value semantics, automatic `IEquatable` implementation, and cleaner rollback logic. +* **State Variables:** Maintain `private State _state;` (current) and `private State _p_state;` (previous valid state). +* **Buffers:** Use `RingBuffer` for sliding window data. +* **Resync:** Implement a periodic full recalculation (e.g., every 1000 ticks) to prevent floating-point drift in running sums. + +### Constructor + +* Validate all parameters (throw `ArgumentException` for invalid values). +* Initialize `Name` property (e.g., `$"Sma({period})"`); +* Support chaining: `public [Name](ITValuePublisher source, ...)` + +### The `Update` Method Contract + +The `Update` method is the heart of the indicator. + +```csharp +public TValue Update(TValue input, bool isNew = true) +``` + +* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +* **Logic:** + 1. **State Rollback:** + + ```csharp + if (isNew) { + _p_state = _state; + // ... update state (e.g. counters) ... + } else { + _state = _p_state; + // ... update state ... + } + ``` + + 2. **Input Validation:** Check `double.IsFinite`. If not, use `_lastValidValue` (stored in `State`). + 3. **Calculation:** Perform the math. + 4. **Publish:** Update `Last` property, invoke `Pub` event, return `Last`. + +### Update Method (TSeries) + +* **Signature:** `public TSeries Update(TSeries source)` +* **Placement:** Must be adjacent to the `Update(TValue)` method. +* **Logic:** + 1. Create output series. + 2. Call static `Calculate(Span)` for performance. + 3. Restore internal state by replaying the last `Period` bars (or full series if recursive). + +### Static Calculate (TSeries) + +* Create a new instance of the indicator. +* Iterate through the source series. +* Return the resulting `TSeries`. + +### Static Calculate (Span) - **Critical for Performance** + +* **Signature:** `public static void Calculate(ReadOnlySpan source, Span output, ...)` +* **Attribute:** `[MethodImpl(MethodImplOptions.AggressiveInlining)]` +* **Optimization:** + * Check for SIMD support (`Avx2.IsSupported`). + * Use `stackalloc` for small buffers (threshold ~256) and for internal state buffers in recursive algorithms where SIMD is not applicable. + * Implement a scalar fallback path that handles `NaN` safely. + * Implement a SIMD path for large, clean datasets (optional but recommended for simple averages). + +## 4. Testing Protocol + +### Unit Tests (`[Name].Tests.cs`) + +* **Framework:** xUnit +* **Data Generation:** Use `GBM` (Geometric Brownian Motion) for generating realistic test data. Avoid using `System.Random` directly. +* **Coverage:** + * Constructor validation (invalid params). + * Basic calculation correctness (compare against manual calc). + * `isNew=true` vs `isNew=false` behavior (bar correction). + * `Reset()` functionality. + * `IsHot` property behavior. + * `NaN` / `Infinity` handling (must not crash, must return finite values). + * Consistency between Object API, Static TSeries API, and Static Span API. + * Edge cases: Period=1, empty input, single input. + +### Validation Tests (`[Name].Validation.Tests.cs`) + +* **Mandatory**: You MUST validate against at least one external authority (TA-Lib, Skender, Tulip, OoplesFinance, Python libs). +* **Tolerance**: Typically `1e-6` to `1e-9`. +* **Data**: Use `ValidationTestData` class which wraps `GBM` (Geometric Brownian Motion) to generate realistic test data and provides pre-calculated Skender quotes. + +#### External Library Usage Guide + +* **Skender.Stock.Indicators:** + * Use `_data.SkenderQuotes.Get[Indicator](...)`. + * Compare using `ValidationHelper.VerifyData`. + +* **TA-Lib (TALib.NETCore):** + * Namespace: `using TALib;` + * Method: `TALib.Functions.[Indicator](...)`. + * Check `Assert.Equal(Core.RetCode.Success, retCode)`. + * Use `ValidationHelper.VerifyData` with `outRange` and `lookback`. + +* **Tulip (Tulip.NETCore):** + * Namespace: `using Tulip;` + * Method: `Tulip.Indicators.[indicator].Run(...)`. + * Handle lookback/offset manually (Tulip output is shorter than input). + * Use `ValidationHelper.VerifyData` with `lookback`. + +* **OoplesFinance.StockIndicators:** + * Namespace: `using OoplesFinance.StockIndicators;` + * Convert data: `_data.SkenderQuotes.Select(q => new TickerData { ... }).ToList()`. + * Use `new StockData(ooplesData).Calculate[Indicator](...)`. + * Compare using `ValidationHelper.VerifyData`. + +## 5. Documentation Standards + +* **Format**: Markdown. +* **Content**: Title, Description, Parameters, Formula (LaTeX), C# Usage Examples. +* **Index**: Add the new indicator to the category index (e.g., `lib/trends/_index.md`) AND the main index (`lib/_index.md`). +* **Linting**: Ensure that markdownlint shows no issues for the file. + * **MD030:** Ensure exactly one space after list markers. + * **MD032:** Ensure lists are surrounded by blank lines. + +## 6. Quantower Adapter + +* **Implementation:** Create a wrapper class in `[Name].Quantower.cs` that adapts the QuanTAlib indicator for the Quantower platform. +* **Tests:** Create unit tests in `[Name].Quantower.Tests.cs` to verify the adapter's functionality using mocks where necessary. + +## 7. Code Review + +* **Tool:** Run CodeRabbit on the changes. +* **Requirement:** Address and fix **ALL** issues identified by the CodeRabbit review before considering the task complete. + +## 8. Development Checklist + +When creating a new indicator, you are **DONE** only when: + +* [ ] Source algorithm is verified. +* [ ] All 6 required files exist. +* [ ] `Update` handles `isNew` and `NaN` correctly. +* [ ] No heap allocations in `Update`. +* [ ] Static `Calculate(Span)` is implemented. +* [ ] Unit tests pass (including edge cases). +* [ ] Validation tests pass against external libs. +* [ ] Documentation is complete and linked in both `_index.md` files. +* [ ] Quantower adapter and tests are implemented. +* [ ] CodeRabbit review issues are resolved. + +## 9. Forbidden Actions + +* **DO NOT** use LINQ in hot paths (`Update` or `Calculate`). +* **DO NOT** use `new` inside `Update`. +* **DO NOT** change `Directory.Build.props` without explicit instruction. +* **DO NOT** remove `[SkipLocalsInit]` or `[MethodImpl]` attributes. +* **DO NOT** ignore `NaN` inputs; handle them safely. + +## 10. Context & Resources + +* **Time**: Use `DateTime.UtcNow`. +* **Math**: Use `System.Math` or `System.Numerics`. +* **Root Namespace**: `QuanTAlib`. diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 00000000..c8561acc --- /dev/null +++ b/docs/API.md @@ -0,0 +1,213 @@ +# QuanTAlib API Documentation + +QuanTAlib employs a **Tri-Modal Architecture** to unify high-performance batch processing with low-latency streaming updates. This design segregates the indicator lifecycle into three distinct mathematical modes, solving the "two-world problem" of quantitative finance (backtesting vs. live trading). + +All indicators inherit from `AbstractBase` and implement the `ITValuePublisher` interface, ensuring a consistent API across the entire library. + +> **Note:** The examples below use `Sma` (Simple Moving Average), but this pattern applies to all indicators in the library. + +## 1. Core Interface (`AbstractBase`) + +Every indicator exposes the following core properties and methods: + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `Name` | `string` | Descriptive name (e.g., `"Sma(14)"`). | +| `Last` | `TValue` | The most recent calculated value (Time + Value). | +| `IsHot` | `bool` | `true` if the indicator has processed enough data to be valid. | +| `WarmupPeriod` | `int` | Number of samples required before `IsHot` becomes true. | +| `Pub` | `event` | Event fired whenever a new value is calculated (Reactive). | + +### Methods + +| Method | Description | +|--------|-------------| +| `Update` | Updates the indicator with a new value (Streaming). | +| `Batch` | Static method for high-performance bulk calculation (Batch). | +| `Prime` | Initializes state from history without full processing (Priming). | +| `Reset` | Resets the indicator to its initial state. | + +--- + +## 2. Mode A: Batch (Stateless) + +**Purpose:** Backtesting, Data Analysis, Optimization +**Method:** `static Batch` + +Batch mode provides stateless, SIMD-accelerated processing of historical arrays. It is optimized for maximum throughput and zero heap allocation. + +### Span-Based (Zero Allocation) + +The most efficient method. Uses SIMD instructions (AVX2/AVX512/Neon) and operates directly on memory spans. + +```csharp +// Signature +public static void Batch(ReadOnlySpan source, Span output, int period); + +// Usage +double[] prices = ...; // Large dataset +double[] results = new double[prices.Length]; + +// Fast, in-place calculation (Zero Allocation) +Sma.Batch(prices, results, 14); +``` + +### TSeries-Based (Convenience) + +A wrapper for `TSeries` objects that returns a new series with aligned timestamps. + +```csharp +TSeries history = ...; +TSeries sma = Sma.Batch(history, 14); +``` + +--- + +## 3. Mode B: Streaming (Stateful) + +**Purpose:** Live Trading, Event Processing +**Method:** `Update` + +Streaming mode handles real-time data ingestion using O(1) complexity per update. It maintains internal state (circular buffers, running sums) to process ticks with minimal latency. + +### Standard Update + +Adds a new value and returns the updated result. + +```csharp +var indicator = new Sma(14); +TValue result = indicator.Update(new TValue(time, price)); +``` + +### Bar Correction (`isNew`) + +Handles intra-bar updates (re-calculation of the current bar) without corrupting state. + +```csharp +// New bar opens +indicator.Update(new TValue(t, 100), isNew: true); + +// Price updates within the same bar (correction) +indicator.Update(new TValue(t, 101), isNew: false); +indicator.Update(new TValue(t, 102), isNew: false); + +// Next bar opens +indicator.Update(new TValue(t+1, 105), isNew: true); +``` + +### Reactive Chaining + +Indicators can subscribe to other `ITValuePublisher` sources (like `TSeries` or other indicators). + +```csharp +TSeries source = ...; + +// Chain: Source -> SMA(14) -> EMA(5) +var sma = new Sma(source, 14); +var ema = new Ema(sma, 5); + +// Updates flow automatically +source.Add(new TValue(time, price)); +// sma updates, then ema updates automatically +``` + +--- + +## 4. Mode C: Priming (The Bridge) + +**Purpose:** Switching from Batch to Streaming +**Method:** `Prime` + +Priming mode hydrates a streaming instance using the minimal required tail of historical data. It calculates the intersection of *History Available* and *State Required*, allowing an indicator to become "Hot" without processing the entire history. + +```csharp +// Signature +public void Prime(ReadOnlySpan source); + +// Usage +var indicator = new Sma(14); +double[] history = ...; // e.g., 100,000 bars + +// Efficiently processes only the last 'period' bars needed to fill the buffer +// O(Warmup) initialization instead of O(History) +indicator.Prime(history); + +// Indicator is now "Hot" and ready for the next live tick +Console.WriteLine(indicator.IsHot); // true +``` + +--- + +## 5. One-Shot Hybrid (`Calculate`) + +A high-level helper that combines Batch and Priming modes. It calculates the entire history and returns a "hot" instance ready for immediate real-time updates. + +```csharp +// Signature +public static (TSeries Results, Sma Indicator) Calculate(TSeries source, int period); + +// Usage +TSeries history = ...; +var (results, indicator) = Sma.Calculate(history, 14); + +// 'results' contains the full calculated history (Batch Mode) +// 'indicator' is fully warmed up (Priming Mode) and ready for live ticks +indicator.Update(newTick); // Streaming Mode +``` + +--- + +## 6. Validity & Convergence (`IsHot`) + +The `IsHot` property indicates whether the indicator has processed enough data to produce mathematically valid results. + +### Streaming Context + +`IsHot` becomes `true` once the required warmup period is satisfied. + +```csharp +var sma = new Sma(10); +// First 9 updates: IsHot = false +// 10th update: IsHot = true +``` + +### Batch Context + +The initial portion of the output contains "cold" values. + +**How many values are cold?** + +- **Fixed-Window** (SMA, RSI): `WarmupPeriod` (usually `period - 1`). +- **Recursive** (EMA, MACD): Technically infinite, practically `3-4 * period`. + +**Checking Validity:** + +- **Property:** Use `WarmupPeriod` to determine how many initial values to skip. +- **Process API:** The returned instance's `IsHot` property confirms if the batch was long enough. + +--- + +## Architecture Diagram + +```mermaid +graph LR + H[Historical Data] + L[Live Data] + + subgraph "Mode A: Batch" + H -->|Batch| R[Backtest Results] + end + + subgraph "Mode C: Priming" + H -->|Prime| S[Hydrated State] + end + + subgraph "Mode B: Streaming" + S --> I[Indicator Instance] + L -->|Update| I + I -->|Update| O[Live Results] + end + + I -.->|IsHot| V[Valid State] diff --git a/lib/_index.md b/lib/_index.md index 7c193e48..06130feb 100644 --- a/lib/_index.md +++ b/lib/_index.md @@ -103,7 +103,7 @@ | HOMOD | Homodyne Discriminator Dominant Cycle | Cycles | | HP | Hodrick-Prescott Filter | Trends | | HPF | Ehlers Highpass Filter | Trends | -| HTIT | Ehlers Hilbert Transform Instantaneous Trend | Trends | +| [HTIT](trends/htit/Htit.md) | Ehlers Hilbert Transform Instantaneous Trend | Trends | | HT_DCPERIOD | Ehlers Hilbert Transform Dominant Cycle Period | Cycles | | HT_DCPHASE | Ehlers Hilbert Transform Dominant Cycle Phase | Cycles | | HT_PHASOR | Ehlers Hilbert Transform Phasor Components | Cycles | @@ -188,7 +188,7 @@ | PVO | Percentage Volume Oscillator | Volume | | PVR | Price Volume Rank | Volume | | PVT | Price Volume Trend | Volume | -| PWMA | Pascal Weighted MA | Trends | +| [PWMA](trends/pwma/Pwma.md) | Pascal Weighted MA | Trends | | QEMA | Quadruple Exponential MA | Trends | | QSTICK | Qstick Indicator | Momentum | | QUANTILE | Quantile | Statistics | diff --git a/lib/core/AbstractBase.cs b/lib/core/AbstractBase.cs new file mode 100644 index 00000000..7e67d639 --- /dev/null +++ b/lib/core/AbstractBase.cs @@ -0,0 +1,69 @@ +using System; + +namespace QuanTAlib; + +/// +/// Abstract base class for all indicators. +/// Enforces a consistent contract for State, Name, WarmupPeriod, and core methods. +/// +public abstract class AbstractBase : ITValuePublisher +{ + /// + /// Display name for the indicator. + /// + public string Name { get; protected set; } = string.Empty; + + /// + /// Number of periods before the indicator is considered "hot" (valid). + /// + public int WarmupPeriod { get; protected set; } + + /// + /// Current value of the indicator. + /// + public TValue Last { get; protected set; } + + /// + /// True if the indicator has enough data to produce valid results. + /// + public abstract bool IsHot { get; } + + /// + /// Event triggered when a new TValue is available. + /// + public event Action? Pub; + + /// + /// Helper to invoke the Pub event. + /// + protected void PubEvent(TValue value) + { + Pub?.Invoke(value); + } + + /// + /// Initializes the indicator state using the provided history. + /// + /// Historical data + public abstract void Prime(ReadOnlySpan source); + + /// + /// Updates the indicator with a single value. + /// + /// Input value + /// True if this is a new bar, False if it's an update to the last bar + /// Updated value + public abstract TValue Update(TValue input, bool isNew = true); + + /// + /// Updates the indicator with a series of values. + /// + /// Input series + /// Series of calculated values + public abstract TSeries Update(TSeries source); + + /// + /// Resets the indicator to its initial state. + /// + public abstract void Reset(); +} diff --git a/lib/core/simd/SimdExtensions.Tests.cs b/lib/core/simd/SimdExtensions.Tests.cs index 038d6093..c4b6a987 100644 --- a/lib/core/simd/SimdExtensions.Tests.cs +++ b/lib/core/simd/SimdExtensions.Tests.cs @@ -525,6 +525,32 @@ public class SimdExtensionsTests Assert.Throws(() => SimdExtensions.Subtract(left, right, result)); } + // DotProduct tests + [Fact] + public void DotProduct_SameLength_CorrectResult() + { + double[] a = [1.0, 2.0, 3.0]; + double[] b = [4.0, 5.0, 6.0]; + // 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32 + Assert.Equal(32.0, SimdExtensions.DotProduct(a, b)); + } + + [Fact] + public void DotProduct_DifferentLengths_ThrowsArgumentException() + { + double[] a = [1.0, 2.0]; + double[] b = [1.0]; + Assert.Throws(() => SimdExtensions.DotProduct(a, b)); + } + + [Fact] + public void DotProduct_EmptySpans_ReturnsZero() + { + double[] a = []; + double[] b = []; + Assert.Equal(0.0, SimdExtensions.DotProduct(a, b)); + } + // Integration tests [Fact] public void SIMD_WorksWithTSeriesValues() diff --git a/lib/core/simd/SimdExtensions.cs b/lib/core/simd/SimdExtensions.cs index 63674ed8..4cf22bff 100644 --- a/lib/core/simd/SimdExtensions.cs +++ b/lib/core/simd/SimdExtensions.cs @@ -1,711 +1,713 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.Arm; -using System.Runtime.Intrinsics.X86; - -namespace QuanTAlib; - -/// -/// SIMD-accelerated extension methods for high-performance array operations. -/// Uses Vector for 4-8x speedup on supported hardware with automatic scalar fallback. -/// -public static class SimdExtensions -{ - // Internal scalar implementations for testability - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool ContainsNonFiniteScalar(ReadOnlySpan span) - { - for (int i = 0; i < span.Length; i++) - { - if (!double.IsFinite(span[i])) - return true; - } - return false; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static double SumScalar(ReadOnlySpan span) - { - double scalar = 0.0; - for (int i = 0; i < span.Length; i++) - scalar += span[i]; - return scalar; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static double MinScalar(ReadOnlySpan span) - { - if (span.Length == 0) - throw new ArgumentException("Span must not be empty", nameof(span)); - - double min = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] < min) - min = span[i]; - } - return min; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static double MaxScalar(ReadOnlySpan span) - { - if (span.Length == 0) - throw new ArgumentException("Span must not be empty", nameof(span)); - - double max = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] > max) - max = span[i]; - } - return max; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static double VarianceScalar(ReadOnlySpan span, double mean) - { - double sumSquares = 0.0; - for (int i = 0; i < span.Length; i++) - { - double diff = span[i] - mean; - sumSquares += diff * diff; - } - return sumSquares / (span.Length - 1); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan span) - { - double scalarMin = span[0]; - double scalarMax = span[0]; - for (int i = 1; i < span.Length; i++) - { - if (span[i] < scalarMin) scalarMin = span[i]; - if (span[i] > scalarMax) scalarMax = span[i]; - } - return (scalarMin, scalarMax); - } - - /// - /// Checks if span contains any non-finite values (NaN or Infinity). - /// Returns true if any non-finite value is found. - /// Uses SIMD: NaN detected via v != v (NaN is the only value where this is true), - /// Infinity detected via |v| > MaxValue comparison. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ContainsNonFinite(this ReadOnlySpan span) - { - if (span.IsEmpty) return false; - - if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) - { - int vectorSize = Vector.Count; - int i = 0; - var maxValue = new Vector(double.MaxValue); - - for (; i <= span.Length - vectorSize; i += vectorSize) - { - var vector = new Vector(span.Slice(i, vectorSize)); - - // NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes - var nanCheck = Vector.Equals(vector, vector); - if (!nanCheck.Equals(Vector.AllBitsSet)) - return true; - - // Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue) - var absVec = Vector.Abs(vector); - var infCheck = Vector.GreaterThan(absVec, maxValue); - if (!infCheck.Equals(Vector.Zero)) - return true; - } - - for (; i < span.Length; i++) - { - if (!double.IsFinite(span[i])) - return true; - } - - return false; - } - - return ContainsNonFiniteScalar(span); - } - - /// - /// Calculates sum using SIMD vectorization when available. - /// 4-8x faster than scalar loop on AVX2/AVX-512 hardware. - /// Returns NaN if any input value is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double SumSIMD(this ReadOnlySpan span) - { - if (span.IsEmpty) return 0.0; - - // Guard against non-finite inputs - if (span.ContainsNonFinite()) return double.NaN; - - if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) - { - Vector sum = Vector.Zero; - int vectorSize = Vector.Count; - int i = 0; - - for (; i <= span.Length - vectorSize; i += vectorSize) - { - var vector = new Vector(span.Slice(i, vectorSize)); - sum += vector; - } - - double result = 0.0; - for (int j = 0; j < vectorSize; j++) - result += sum[j]; - - for (; i < span.Length; i++) - result += span[i]; - - return result; - } - - return SumScalar(span); - } - - /// - /// Calculates minimum value using SIMD vectorization when available. - /// 4-6x faster than scalar loop on AVX2/AVX-512 hardware. - /// Returns NaN if any input value is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double MinSIMD(this ReadOnlySpan span) - { - if (span.IsEmpty) return double.NaN; - if (span.Length == 1) return span[0]; - - // Guard against non-finite inputs - if (span.ContainsNonFinite()) return double.NaN; - - if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) - { - int vectorSize = Vector.Count; - var minVec = new Vector(span.Slice(0, vectorSize)); - int i = vectorSize; - - for (; i <= span.Length - vectorSize; i += vectorSize) - { - var vector = new Vector(span.Slice(i, vectorSize)); - minVec = Vector.Min(minVec, vector); - } - - double result = minVec[0]; - for (int j = 1; j < vectorSize; j++) - { - if (minVec[j] < result) - result = minVec[j]; - } - - for (; i < span.Length; i++) - { - if (span[i] < result) - result = span[i]; - } - - return result; - } - - return MinScalar(span); - } - - /// - /// Calculates maximum value using SIMD vectorization when available. - /// 4-6x faster than scalar loop on AVX2/AVX-512 hardware. - /// Returns NaN if any input value is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double MaxSIMD(this ReadOnlySpan span) - { - if (span.IsEmpty) return double.NaN; - if (span.Length == 1) return span[0]; - - // Guard against non-finite inputs - if (span.ContainsNonFinite()) return double.NaN; - - if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) - { - int vectorSize = Vector.Count; - var maxVec = new Vector(span.Slice(0, vectorSize)); - int i = vectorSize; - - for (; i <= span.Length - vectorSize; i += vectorSize) - { - var vector = new Vector(span.Slice(i, vectorSize)); - maxVec = Vector.Max(maxVec, vector); - } - - double result = maxVec[0]; - for (int j = 1; j < vectorSize; j++) - { - if (maxVec[j] > result) - result = maxVec[j]; - } - - for (; i < span.Length; i++) - { - if (span[i] > result) - result = span[i]; - } - - return result; - } - - return MaxScalar(span); - } - - /// - /// Calculates average using SIMD vectorization when available. - /// 4-8x faster than scalar loop on AVX2/AVX-512 hardware. - /// Returns NaN if any input value is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double AverageSIMD(this ReadOnlySpan span) - { - if (span.IsEmpty) return double.NaN; - // SumSIMD already guards against non-finite, which will propagate NaN - return span.SumSIMD() / span.Length; - } - - /// - /// Calculates variance using a two-pass SIMD variant that computes the mean first (via AverageSIMD) and then sums squared differences to produce variance. - /// Note that this is not the single-pass Welford algorithm. - /// Returns NaN if any input value is non-finite or if mean is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double VarianceSIMD(this ReadOnlySpan span, double? mean = null) - { - if (span.Length < 2) return double.NaN; - - double m; - if (mean.HasValue) - { - if (span.ContainsNonFinite()) return double.NaN; - m = mean.Value; - } - else - { - m = span.AverageSIMD(); - } - - if (!double.IsFinite(m)) return double.NaN; - - if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) - { - var meanVec = new Vector(m); - Vector sumSq = Vector.Zero; - int vectorSize = Vector.Count; - int i = 0; - - for (; i <= span.Length - vectorSize; i += vectorSize) - { - var vector = new Vector(span.Slice(i, vectorSize)); - var diff = vector - meanVec; - sumSq += diff * diff; - } - - double result = 0.0; - for (int j = 0; j < vectorSize; j++) - result += sumSq[j]; - - for (; i < span.Length; i++) - { - double diff = span[i] - m; - result += diff * diff; - } - - return result / (span.Length - 1); - } - - return VarianceScalar(span, m); - } - - /// - /// Calculates standard deviation using SIMD vectorization. - /// Returns NaN if any input value is non-finite or if mean is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double StdDevSIMD(this ReadOnlySpan span, double? mean = null) - { - // VarianceSIMD already guards against non-finite, which will propagate NaN through Sqrt - return Math.Sqrt(span.VarianceSIMD(mean)); - } - - /// - /// Finds both min and max in a single pass using SIMD vectorization. - /// More efficient than calling MinSIMD and MaxSIMD separately. - /// Returns (NaN, NaN) if any input value is non-finite. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan span) - { - if (span.IsEmpty) return (double.NaN, double.NaN); - if (span.Length == 1) return (span[0], span[0]); - - // Guard against non-finite inputs - if (span.ContainsNonFinite()) return (double.NaN, double.NaN); - - if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) - { - int vectorSize = Vector.Count; - var minVec = new Vector(span.Slice(0, vectorSize)); - var maxVec = minVec; - int i = vectorSize; - - for (; i <= span.Length - vectorSize; i += vectorSize) - { - var vector = new Vector(span.Slice(i, vectorSize)); - minVec = Vector.Min(minVec, vector); - maxVec = Vector.Max(maxVec, vector); - } - - double min = minVec[0]; - double max = maxVec[0]; - for (int j = 1; j < vectorSize; j++) - { - if (minVec[j] < min) min = minVec[j]; - if (maxVec[j] > max) max = maxVec[j]; - } - - for (; i < span.Length; i++) - { - if (span[i] < min) min = span[i]; - if (span[i] > max) max = span[i]; - } - - return (min, max); - } - - return MinMaxScalar(span); - } - - /// - /// Element-wise addition of two spans using SIMD. - /// result[i] = left[i] + right[i] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Add(ReadOnlySpan left, ReadOnlySpan right, Span result) - { - if (left.Length != right.Length || left.Length != result.Length) - throw new ArgumentException("All spans must have the same length"); - - int i = 0; - if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count) - { - int vectorSize = Vector.Count; - for (; i <= left.Length - vectorSize; i += vectorSize) - { - var vLeft = new Vector(left.Slice(i, vectorSize)); - var vRight = new Vector(right.Slice(i, vectorSize)); - (vLeft + vRight).CopyTo(result.Slice(i, vectorSize)); - } - } - - for (; i < left.Length; i++) - { - result[i] = left[i] + right[i]; - } - } - - /// - /// Element-wise subtraction of two spans using SIMD. - /// result[i] = left[i] - right[i] - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Subtract(ReadOnlySpan left, ReadOnlySpan right, Span result) - { - if (left.Length != right.Length || left.Length != result.Length) - throw new ArgumentException("All spans must have the same length"); - - int i = 0; - if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count) - { - int vectorSize = Vector.Count; - for (; i <= left.Length - vectorSize; i += vectorSize) - { - var vLeft = new Vector(left.Slice(i, vectorSize)); - var vRight = new Vector(right.Slice(i, vectorSize)); - (vLeft - vRight).CopyTo(result.Slice(i, vectorSize)); - } - } - - for (; i < left.Length; i++) - { - result[i] = left[i] - right[i]; - } - } - - /// - /// Calculates the dot product of two spans using SIMD intrinsics. - /// Supports AVX512, AVX2, and NEON (ARM64). - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static double DotProduct(this ReadOnlySpan a, ReadOnlySpan b) - { - if (a.Length != b.Length || a.Length == 0) return 0; - - int len = a.Length; - - // Fast path for very small kernels (avoid SIMD overhead) - if (len <= 3) - { - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - double sum = aRef * bRef; - if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); - if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); - return sum; - } - - if (Avx512F.IsSupported) - return DotProductAvx512(a, b); - - if (Avx2.IsSupported) - return DotProductAvx2(a, b); - - if (AdvSimd.Arm64.IsSupported) - return DotProductNeon(a, b); - - double s1 = 0, s2 = 0, s3 = 0, s4 = 0; - ref double ar = ref MemoryMarshal.GetReference(a); - ref double br = ref MemoryMarshal.GetReference(b); - - int i = 0; - // Unroll scalar loop with 4 accumulators to break dependency chains - for (; i <= len - 4; i += 4) - { - s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); - s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1); - s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2); - s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3); - } - - double s = s1 + s2 + s3 + s4; - - for (; i < len; i++) - { - s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); - } - return s; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductAvx512(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector512 vSum = Vector512.Zero; - Vector512 vSum2 = Vector512.Zero; - Vector512 vSum3 = Vector512.Zero; - Vector512 vSum4 = Vector512.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Unroll loop: Process 32 doubles (4 vectors) at a time - if (len >= 32) - { - for (; i <= len - 32; i += 32) - { - var va1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - var va2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); - var vb2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); - - var va3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 16)); - var vb3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 16)); - - var va4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 24)); - var vb4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 24)); - - vSum = Avx512F.FusedMultiplyAdd(va1, vb1, vSum); - vSum2 = Avx512F.FusedMultiplyAdd(va2, vb2, vSum2); - vSum3 = Avx512F.FusedMultiplyAdd(va3, vb3, vSum3); - vSum4 = Avx512F.FusedMultiplyAdd(va4, vb4, vSum4); - } - } - - // Process remaining vectors (8 doubles at a time) - for (; i <= len - 8; i += 8) - { - var va = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - vSum = Avx512F.FusedMultiplyAdd(va, vb, vSum); - } - - // Combine accumulators - vSum = Avx512F.Add(vSum, vSum2); - vSum3 = Avx512F.Add(vSum3, vSum4); - vSum = Avx512F.Add(vSum, vSum3); - - // Horizontal sum - reduce to Vector256, then Vector128 - Vector256 v256 = Avx512F.Add(vSum.GetLower(), vSum.GetUpper()); - Vector128 lower = v256.GetLower(); - Vector128 upper = v256.GetUpper(); - Vector128 combined = Sse2.Add(lower, upper); - double sum = combined.GetElement(0) + combined.GetElement(1); - - // Scalar remainder - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductAvx2(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector256 vSum = Vector256.Zero; - Vector256 vSum2 = Vector256.Zero; - Vector256 vSum3 = Vector256.Zero; - Vector256 vSum4 = Vector256.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Unroll loop: Process 16 doubles (4 vectors) at a time - if (len >= 16) - { - for (; i <= len - 16; i += 16) - { - var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); - var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); - - var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); - var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); - - var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12)); - var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12)); - - if (Fma.IsSupported) - { - vSum = Fma.MultiplyAdd(va1, vb1, vSum); - vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); - vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3); - vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4); - } - else - { - vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1)); - vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2)); - vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3)); - vSum4 = Avx.Add(vSum4, Avx.Multiply(va4, vb4)); - } - } - } - - // Process remaining vectors (4 doubles at a time) - for (; i <= len - 4; i += 4) - { - var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - vSum = Fma.IsSupported - ? Fma.MultiplyAdd(va, vb, vSum) - : Avx.Add(vSum, Avx.Multiply(va, vb)); - } - - // Combine accumulators - vSum = Avx.Add(vSum, vSum2); - vSum3 = Avx.Add(vSum3, vSum4); - vSum = Avx.Add(vSum, vSum3); - - // Horizontal sum - Vector128 lower = vSum.GetLower(); - Vector128 upper = vSum.GetUpper(); - Vector128 combined = Sse2.Add(lower, upper); - double sum = combined.GetElement(0) + combined.GetElement(1); - - // Process remaining elements (scalar) - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double DotProductNeon(ReadOnlySpan a, ReadOnlySpan b) - { - int len = a.Length; - int i = 0; - Vector128 vSum = Vector128.Zero; - Vector128 vSum2 = Vector128.Zero; - Vector128 vSum3 = Vector128.Zero; - Vector128 vSum4 = Vector128.Zero; - - ref double aRef = ref MemoryMarshal.GetReference(a); - ref double bRef = ref MemoryMarshal.GetReference(b); - - // Unroll loop: Process 8 doubles (4 vectors) at a time - if (len >= 8) - { - for (; i <= len - 8; i += 8) - { - var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); - var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); - - var va3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); - var vb3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); - - var va4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 6)); - var vb4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 6)); - - // NEON has FMA on ARM64 - // Since we are inside DotProductNeon which is guarded by AdvSimd.Arm64.IsSupported, - // we can assume Arm64 support. - vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va1, vb1); - vSum2 = AdvSimd.Arm64.FusedMultiplyAdd(vSum2, va2, vb2); - vSum3 = AdvSimd.Arm64.FusedMultiplyAdd(vSum3, va3, vb3); - vSum4 = AdvSimd.Arm64.FusedMultiplyAdd(vSum4, va4, vb4); - } - } - - // Process remaining vectors (2 doubles at a time) - for (; i <= len - 2; i += 2) - { - var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); - var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); - - vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va, vb); - } - - // Combine accumulators - vSum = AdvSimd.Arm64.Add(vSum, vSum2); - vSum3 = AdvSimd.Arm64.Add(vSum3, vSum4); - vSum = AdvSimd.Arm64.Add(vSum, vSum3); - - // Horizontal sum (NEON has pairwise add) - double sum = AdvSimd.Arm64.AddPairwiseScalar(vSum).ToScalar(); - - // Scalar remainder (0-1 elements) - for (; i < len; i++) - { - sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); - } - - return sum; - } - -} +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace QuanTAlib; + +/// +/// SIMD-accelerated extension methods for high-performance array operations. +/// Uses Vector for 4-8x speedup on supported hardware with automatic scalar fallback. +/// +public static class SimdExtensions +{ + // Internal scalar implementations for testability + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool ContainsNonFiniteScalar(ReadOnlySpan span) + { + for (int i = 0; i < span.Length; i++) + { + if (!double.IsFinite(span[i])) + return true; + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double SumScalar(ReadOnlySpan span) + { + double scalar = 0.0; + for (int i = 0; i < span.Length; i++) + scalar += span[i]; + return scalar; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double MinScalar(ReadOnlySpan span) + { + if (span.Length == 0) + throw new ArgumentException("Span must not be empty", nameof(span)); + + double min = span[0]; + for (int i = 1; i < span.Length; i++) + { + if (span[i] < min) + min = span[i]; + } + return min; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double MaxScalar(ReadOnlySpan span) + { + if (span.Length == 0) + throw new ArgumentException("Span must not be empty", nameof(span)); + + double max = span[0]; + for (int i = 1; i < span.Length; i++) + { + if (span[i] > max) + max = span[i]; + } + return max; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static double VarianceScalar(ReadOnlySpan span, double mean) + { + double sumSquares = 0.0; + for (int i = 0; i < span.Length; i++) + { + double diff = span[i] - mean; + sumSquares += diff * diff; + } + return sumSquares / (span.Length - 1); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static (double Min, double Max) MinMaxScalar(ReadOnlySpan span) + { + double scalarMin = span[0]; + double scalarMax = span[0]; + for (int i = 1; i < span.Length; i++) + { + if (span[i] < scalarMin) scalarMin = span[i]; + if (span[i] > scalarMax) scalarMax = span[i]; + } + return (scalarMin, scalarMax); + } + + /// + /// Checks if span contains any non-finite values (NaN or Infinity). + /// Returns true if any non-finite value is found. + /// Uses SIMD: NaN detected via v != v (NaN is the only value where this is true), + /// Infinity detected via |v| > MaxValue comparison. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ContainsNonFinite(this ReadOnlySpan span) + { + if (span.IsEmpty) return false; + + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + int vectorSize = Vector.Count; + int i = 0; + var maxValue = new Vector(double.MaxValue); + + for (; i <= span.Length - vectorSize; i += vectorSize) + { + var vector = new Vector(span.Slice(i, vectorSize)); + + // NaN check: NaN != NaN, so Vector.Equals(v, v) will be false for NaN lanes + var nanCheck = Vector.Equals(vector, vector); + if (!nanCheck.Equals(Vector.AllBitsSet)) + return true; + + // Infinity check: |v| > MaxValue (Infinity has magnitude > MaxValue) + var absVec = Vector.Abs(vector); + var infCheck = Vector.GreaterThan(absVec, maxValue); + if (!infCheck.Equals(Vector.Zero)) + return true; + } + + for (; i < span.Length; i++) + { + if (!double.IsFinite(span[i])) + return true; + } + + return false; + } + + return ContainsNonFiniteScalar(span); + } + + /// + /// Calculates sum using SIMD vectorization when available. + /// 4-8x faster than scalar loop on AVX2/AVX-512 hardware. + /// Returns NaN if any input value is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double SumSIMD(this ReadOnlySpan span) + { + if (span.IsEmpty) return 0.0; + + // Guard against non-finite inputs + if (span.ContainsNonFinite()) return double.NaN; + + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + Vector sum = Vector.Zero; + int vectorSize = Vector.Count; + int i = 0; + + for (; i <= span.Length - vectorSize; i += vectorSize) + { + var vector = new Vector(span.Slice(i, vectorSize)); + sum += vector; + } + + double result = 0.0; + for (int j = 0; j < vectorSize; j++) + result += sum[j]; + + for (; i < span.Length; i++) + result += span[i]; + + return result; + } + + return SumScalar(span); + } + + /// + /// Calculates minimum value using SIMD vectorization when available. + /// 4-6x faster than scalar loop on AVX2/AVX-512 hardware. + /// Returns NaN if any input value is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MinSIMD(this ReadOnlySpan span) + { + if (span.IsEmpty) return double.NaN; + if (span.Length == 1) return span[0]; + + // Guard against non-finite inputs + if (span.ContainsNonFinite()) return double.NaN; + + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + int vectorSize = Vector.Count; + var minVec = new Vector(span[..vectorSize]); + int i = vectorSize; + + for (; i <= span.Length - vectorSize; i += vectorSize) + { + var vector = new Vector(span.Slice(i, vectorSize)); + minVec = Vector.Min(minVec, vector); + } + + double result = minVec[0]; + for (int j = 1; j < vectorSize; j++) + { + if (minVec[j] < result) + result = minVec[j]; + } + + for (; i < span.Length; i++) + { + if (span[i] < result) + result = span[i]; + } + + return result; + } + + return MinScalar(span); + } + + /// + /// Calculates maximum value using SIMD vectorization when available. + /// 4-6x faster than scalar loop on AVX2/AVX-512 hardware. + /// Returns NaN if any input value is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double MaxSIMD(this ReadOnlySpan span) + { + if (span.IsEmpty) return double.NaN; + if (span.Length == 1) return span[0]; + + // Guard against non-finite inputs + if (span.ContainsNonFinite()) return double.NaN; + + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + int vectorSize = Vector.Count; + var maxVec = new Vector(span[..vectorSize]); + int i = vectorSize; + + for (; i <= span.Length - vectorSize; i += vectorSize) + { + var vector = new Vector(span.Slice(i, vectorSize)); + maxVec = Vector.Max(maxVec, vector); + } + + double result = maxVec[0]; + for (int j = 1; j < vectorSize; j++) + { + if (maxVec[j] > result) + result = maxVec[j]; + } + + for (; i < span.Length; i++) + { + if (span[i] > result) + result = span[i]; + } + + return result; + } + + return MaxScalar(span); + } + + /// + /// Calculates average using SIMD vectorization when available. + /// 4-8x faster than scalar loop on AVX2/AVX-512 hardware. + /// Returns NaN if any input value is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double AverageSIMD(this ReadOnlySpan span) + { + if (span.IsEmpty) return double.NaN; + // SumSIMD already guards against non-finite, which will propagate NaN + return span.SumSIMD() / span.Length; + } + + /// + /// Calculates variance using a two-pass SIMD variant that computes the mean first (via AverageSIMD) and then sums squared differences to produce variance. + /// Note that this is not the single-pass Welford algorithm. + /// Returns NaN if any input value is non-finite or if mean is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double VarianceSIMD(this ReadOnlySpan span, double? mean = null) + { + if (span.Length < 2) return double.NaN; + + double m; + if (mean.HasValue) + { + if (span.ContainsNonFinite()) return double.NaN; + m = mean.Value; + } + else + { + m = span.AverageSIMD(); + } + + if (!double.IsFinite(m)) return double.NaN; + + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + var meanVec = new Vector(m); + Vector sumSq = Vector.Zero; + int vectorSize = Vector.Count; + int i = 0; + + for (; i <= span.Length - vectorSize; i += vectorSize) + { + var vector = new Vector(span.Slice(i, vectorSize)); + var diff = vector - meanVec; + sumSq += diff * diff; + } + + double result = 0.0; + for (int j = 0; j < vectorSize; j++) + result += sumSq[j]; + + for (; i < span.Length; i++) + { + double diff = span[i] - m; + result += diff * diff; + } + + return result / (span.Length - 1); + } + + return VarianceScalar(span, m); + } + + /// + /// Calculates standard deviation using SIMD vectorization. + /// Returns NaN if any input value is non-finite or if mean is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double StdDevSIMD(this ReadOnlySpan span, double? mean = null) + { + // VarianceSIMD already guards against non-finite, which will propagate NaN through Sqrt + return Math.Sqrt(span.VarianceSIMD(mean)); + } + + /// + /// Finds both min and max in a single pass using SIMD vectorization. + /// More efficient than calling MinSIMD and MaxSIMD separately. + /// Returns (NaN, NaN) if any input value is non-finite. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan span) + { + if (span.IsEmpty) return (double.NaN, double.NaN); + if (span.Length == 1) return (span[0], span[0]); + + // Guard against non-finite inputs + if (span.ContainsNonFinite()) return (double.NaN, double.NaN); + + if (Vector.IsHardwareAccelerated && span.Length >= Vector.Count) + { + int vectorSize = Vector.Count; + var minVec = new Vector(span[..vectorSize]); + var maxVec = minVec; + int i = vectorSize; + + for (; i <= span.Length - vectorSize; i += vectorSize) + { + var vector = new Vector(span.Slice(i, vectorSize)); + minVec = Vector.Min(minVec, vector); + maxVec = Vector.Max(maxVec, vector); + } + + double min = minVec[0]; + double max = maxVec[0]; + for (int j = 1; j < vectorSize; j++) + { + if (minVec[j] < min) min = minVec[j]; + if (maxVec[j] > max) max = maxVec[j]; + } + + for (; i < span.Length; i++) + { + if (span[i] < min) min = span[i]; + if (span[i] > max) max = span[i]; + } + + return (min, max); + } + + return MinMaxScalar(span); + } + + /// + /// Element-wise addition of two spans using SIMD. + /// result[i] = left[i] + right[i] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ReadOnlySpan left, ReadOnlySpan right, Span result) + { + if (left.Length != right.Length || left.Length != result.Length) + throw new ArgumentException("All spans must have the same length"); + + int i = 0; + if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count) + { + int vectorSize = Vector.Count; + for (; i <= left.Length - vectorSize; i += vectorSize) + { + var vLeft = new Vector(left.Slice(i, vectorSize)); + var vRight = new Vector(right.Slice(i, vectorSize)); + (vLeft + vRight).CopyTo(result.Slice(i, vectorSize)); + } + } + + for (; i < left.Length; i++) + { + result[i] = left[i] + right[i]; + } + } + + /// + /// Element-wise subtraction of two spans using SIMD. + /// result[i] = left[i] - right[i] + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Subtract(ReadOnlySpan left, ReadOnlySpan right, Span result) + { + if (left.Length != right.Length || left.Length != result.Length) + throw new ArgumentException("All spans must have the same length"); + + int i = 0; + if (Vector.IsHardwareAccelerated && left.Length >= Vector.Count) + { + int vectorSize = Vector.Count; + for (; i <= left.Length - vectorSize; i += vectorSize) + { + var vLeft = new Vector(left.Slice(i, vectorSize)); + var vRight = new Vector(right.Slice(i, vectorSize)); + (vLeft - vRight).CopyTo(result.Slice(i, vectorSize)); + } + } + + for (; i < left.Length; i++) + { + result[i] = left[i] - right[i]; + } + } + + /// + /// Calculates the dot product of two spans using SIMD intrinsics. + /// Supports AVX512, AVX2, and NEON (ARM64). + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static double DotProduct(this ReadOnlySpan a, ReadOnlySpan b) + { + if (a.Length != b.Length) + throw new ArgumentException("Spans must have equal length"); + + if (a.IsEmpty) return 0.0; + + int len = a.Length; + + // Fast path for very small kernels (avoid SIMD overhead) + if (len <= 3) + { + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + double sum = aRef * bRef; + if (len > 1) sum += Unsafe.Add(ref aRef, 1) * Unsafe.Add(ref bRef, 1); + if (len > 2) sum += Unsafe.Add(ref aRef, 2) * Unsafe.Add(ref bRef, 2); + return sum; + } + + if (Avx512F.IsSupported) + return DotProductAvx512(a, b); + + if (Avx2.IsSupported) + return DotProductAvx2(a, b); + + if (AdvSimd.Arm64.IsSupported) + return DotProductNeon(a, b); + + double s1 = 0, s2 = 0, s3 = 0, s4 = 0; + ref double ar = ref MemoryMarshal.GetReference(a); + ref double br = ref MemoryMarshal.GetReference(b); + + int i = 0; + // Unroll scalar loop with 4 accumulators to break dependency chains + for (; i <= len - 4; i += 4) + { + s1 += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + s2 += Unsafe.Add(ref ar, i + 1) * Unsafe.Add(ref br, i + 1); + s3 += Unsafe.Add(ref ar, i + 2) * Unsafe.Add(ref br, i + 2); + s4 += Unsafe.Add(ref ar, i + 3) * Unsafe.Add(ref br, i + 3); + } + + double s = s1 + s2 + s3 + s4; + + for (; i < len; i++) + { + s += Unsafe.Add(ref ar, i) * Unsafe.Add(ref br, i); + } + return s; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductAvx512(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector512 vSum = Vector512.Zero; + Vector512 vSum2 = Vector512.Zero; + Vector512 vSum3 = Vector512.Zero; + Vector512 vSum4 = Vector512.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 32 doubles (4 vectors) at a time + if (len >= 32) + { + for (; i <= len - 32; i += 32) + { + var va1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); + var vb2 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); + + var va3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 16)); + var vb3 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 16)); + + var va4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 24)); + var vb4 = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 24)); + + vSum = Avx512F.FusedMultiplyAdd(va1, vb1, vSum); + vSum2 = Avx512F.FusedMultiplyAdd(va2, vb2, vSum2); + vSum3 = Avx512F.FusedMultiplyAdd(va3, vb3, vSum3); + vSum4 = Avx512F.FusedMultiplyAdd(va4, vb4, vSum4); + } + } + + // Process remaining vectors (8 doubles at a time) + for (; i <= len - 8; i += 8) + { + var va = Vector512.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector512.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + vSum = Avx512F.FusedMultiplyAdd(va, vb, vSum); + } + + // Combine accumulators + vSum = Avx512F.Add(vSum, vSum2); + vSum3 = Avx512F.Add(vSum3, vSum4); + vSum = Avx512F.Add(vSum, vSum3); + + Vector256 v256 = Avx512F.Add(vSum.GetLower(), vSum.GetUpper()); + Vector128 lower = v256.GetLower(); + Vector128 upper = v256.GetUpper(); + Vector128 combined = Sse2.Add(lower, upper); + double sum = combined.GetElement(0) + combined.GetElement(1); + + // Scalar remainder + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductAvx2(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector256 vSum = Vector256.Zero; + Vector256 vSum2 = Vector256.Zero; + Vector256 vSum3 = Vector256.Zero; + Vector256 vSum4 = Vector256.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 16 doubles (4 vectors) at a time + if (len >= 16) + { + for (; i <= len - 16; i += 16) + { + var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); + var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); + + var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8)); + var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8)); + + var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12)); + var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12)); + + if (Fma.IsSupported) + { + vSum = Fma.MultiplyAdd(va1, vb1, vSum); + vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2); + vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3); + vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4); + } + else + { + vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1)); + vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2)); + vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3)); + vSum4 = Avx.Add(vSum4, Avx.Multiply(va4, vb4)); + } + } + } + + // Process remaining vectors (4 doubles at a time) + for (; i <= len - 4; i += 4) + { + var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = Fma.IsSupported + ? Fma.MultiplyAdd(va, vb, vSum) + : Avx.Add(vSum, Avx.Multiply(va, vb)); + } + + // Combine accumulators + vSum = Avx.Add(vSum, vSum2); + vSum3 = Avx.Add(vSum3, vSum4); + vSum = Avx.Add(vSum, vSum3); + + // Horizontal sum + Vector128 lower = vSum.GetLower(); + Vector128 upper = vSum.GetUpper(); + Vector128 combined = Sse2.Add(lower, upper); + double sum = combined.GetElement(0) + combined.GetElement(1); + + // Process remaining elements (scalar) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double DotProductNeon(ReadOnlySpan a, ReadOnlySpan b) + { + int len = a.Length; + int i = 0; + Vector128 vSum = Vector128.Zero; + Vector128 vSum2 = Vector128.Zero; + Vector128 vSum3 = Vector128.Zero; + Vector128 vSum4 = Vector128.Zero; + + ref double aRef = ref MemoryMarshal.GetReference(a); + ref double bRef = ref MemoryMarshal.GetReference(b); + + // Unroll loop: Process 8 doubles (4 vectors) at a time + if (len >= 8) + { + for (; i <= len - 8; i += 8) + { + var va1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb1 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + var va2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 2)); + var vb2 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 2)); + + var va3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4)); + var vb3 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4)); + + var va4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 6)); + var vb4 = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 6)); + + // NEON has FMA on ARM64 + // Since we are inside DotProductNeon which is guarded by AdvSimd.Arm64.IsSupported, + // we can assume Arm64 support. + vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va1, vb1); + vSum2 = AdvSimd.Arm64.FusedMultiplyAdd(vSum2, va2, vb2); + vSum3 = AdvSimd.Arm64.FusedMultiplyAdd(vSum3, va3, vb3); + vSum4 = AdvSimd.Arm64.FusedMultiplyAdd(vSum4, va4, vb4); + } + } + + // Process remaining vectors (2 doubles at a time) + for (; i <= len - 2; i += 2) + { + var va = Vector128.LoadUnsafe(ref Unsafe.Add(ref aRef, i)); + var vb = Vector128.LoadUnsafe(ref Unsafe.Add(ref bRef, i)); + + vSum = AdvSimd.Arm64.FusedMultiplyAdd(vSum, va, vb); + } + + // Combine accumulators + vSum = AdvSimd.Arm64.Add(vSum, vSum2); + vSum3 = AdvSimd.Arm64.Add(vSum3, vSum4); + vSum = AdvSimd.Arm64.Add(vSum, vSum3); + + // Horizontal sum (NEON has pairwise add) + double sum = AdvSimd.Arm64.AddPairwiseScalar(vSum).ToScalar(); + + // Scalar remainder (0-1 elements) + for (; i < len; i++) + { + sum += Unsafe.Add(ref aRef, i) * Unsafe.Add(ref bRef, i); + } + + return sum; + } + +} diff --git a/lib/core/tbar/tbar.cs b/lib/core/tbar/tbar.cs index d2a42720..37bd3781 100644 --- a/lib/core/tbar/tbar.cs +++ b/lib/core/tbar/tbar.cs @@ -1,83 +1,83 @@ -using System.Runtime.CompilerServices; - -namespace QuanTAlib; - -/// -/// A lightweight struct representing an OHLCV bar. -/// Pure data type: 48 bytes (long + 5 doubles). -/// -[SkipLocalsInit] -public readonly struct TBar : IEquatable -{ - public readonly long Time; - public readonly double Open; - public readonly double High; - public readonly double Low; - public readonly double Close; - public readonly double Volume; - - public DateTime AsDateTime => new(Time, DateTimeKind.Utc); - - // TValue conversions (Zero-copy / lightweight creation) - public TValue O { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Open); } - public TValue H { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, High); } - public TValue L { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Low); } - public TValue C { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Close); } - public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); } - - // Computed properties (calculated on demand, no storage overhead) - public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; } - public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; } - public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * 0.333333333333333333; } - public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * 0.333333333333333333; } - public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; } - public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TBar(long time, double open, double high, double low, double close, double volume) - { - Time = time; - Open = open; - High = high; - Low = low; - Close = close; - Volume = volume; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TBar(DateTime time, double open, double high, double low, double close, double volume) - { - Time = time.Ticks; - Open = open; - High = high; - Low = low; - Close = close; - Volume = volume; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator double(TBar bar) => bar.Close; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(TBar other) => - Time == other.Time && - Open == other.Open && - High == other.High && - Low == other.Low && - Close == other.Close && - Volume == other.Volume; - - public override bool Equals(object? obj) => obj is TBar other && Equals(other); - public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume); - public static bool operator ==(TBar left, TBar right) => left.Equals(right); - public static bool operator !=(TBar left, TBar right) => !left.Equals(right); -} +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// A lightweight struct representing an OHLCV bar. +/// Pure data type: 48 bytes (long + 5 doubles). +/// +[SkipLocalsInit] +public readonly struct TBar : IEquatable +{ + public readonly long Time; + public readonly double Open; + public readonly double High; + public readonly double Low; + public readonly double Close; + public readonly double Volume; + + public DateTime AsDateTime => new(Time, DateTimeKind.Utc); + + // TValue conversions (Zero-copy / lightweight creation) + public TValue O { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Open); } + public TValue H { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, High); } + public TValue L { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Low); } + public TValue C { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Close); } + public TValue V { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Time, Volume); } + + // Computed properties (calculated on demand, no storage overhead) + public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; } + public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; } + public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * 0.333333333333333333; } + public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * 0.333333333333333333; } + public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; } + public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(long time, double open, double high, double low, double close, double volume) + { + Time = time; + Open = open; + High = high; + Low = low; + Close = close; + Volume = volume; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TBar(DateTime time, double open, double high, double low, double close, double volume) + { + Time = time.Ticks; + Open = open; + High = high; + Low = low; + Close = close; + Volume = volume; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(TBar bar) => bar.Close; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator TValue(TBar bar) => new(bar.Time, bar.Close); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(TBar bar) => new(bar.Time, DateTimeKind.Utc); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}: O={Open:F2}, H={High:F2}, L={Low:F2}, C={Close:F2}, V={Volume:F2}]"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(TBar other) => + Time == other.Time && + Open == other.Open && + High == other.High && + Low == other.Low && + Close == other.Close && + Volume == other.Volume; + + public override bool Equals(object? obj) => obj is TBar other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Time, Open, High, Low, Close, Volume); + public static bool operator ==(TBar left, TBar right) => left.Equals(right); + public static bool operator !=(TBar left, TBar right) => !left.Equals(right); +} diff --git a/lib/core/tbarseries/tbarseries.cs b/lib/core/tbarseries/tbarseries.cs index 6ee498ff..406d4f3b 100644 --- a/lib/core/tbarseries/tbarseries.cs +++ b/lib/core/tbarseries/tbarseries.cs @@ -1,147 +1,147 @@ -using System.Collections; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace QuanTAlib; - -/// -/// A high-performance OHLCV time series implementation using Structure of Arrays (SoA) layout. -/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency. -/// Exposes TSeries views for each component that share the underlying Time array. -/// -public class TBarSeries : IReadOnlyList -{ - protected readonly List _t; - protected readonly List _o; - protected readonly List _h; - protected readonly List _l; - protected readonly List _c; - protected readonly List _v; - - public string Name { get; set; } = "Bar"; - public event Action? Pub; - - // Note: These views share underlying storage. Do not modify directly; use TBarSeries.Add() instead. - public TSeries Open { get; } - public TSeries High { get; } - public TSeries Low { get; } - public TSeries Close { get; } - public TSeries Volume { get; } - // Aliases for convenience - public TSeries O => Open; - public TSeries H => High; - public TSeries L => Low; - public TSeries C => Close; - public TSeries V => Volume; - - public TBarSeries() : this(0) - { - } - - public TBarSeries(int capacity) - { - _t = new List(capacity); - _o = new List(capacity); - _h = new List(capacity); - _l = new List(capacity); - _c = new List(capacity); - _v = new List(capacity); - - Open = new TSeries(_t, _o) { Name = "Open" }; - High = new TSeries(_t, _h) { Name = "High" }; - Low = new TSeries(_t, _l) { Name = "Low" }; - Close = new TSeries(_t, _c) { Name = "Close" }; - Volume = new TSeries(_t, _v) { Name = "Volume" }; - } - - public int Count - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _c.Count; - } - - public TBar this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(_t[index], _o[index], _h[index], _l[index], _c[index], _v[index]); - } - - public TBar Last - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _c.Count > 0 ? new(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]) : default; - } - - public long LastTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _t.Count > 0 ? _t[^1] : 0; } - public double LastOpen { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _o.Count > 0 ? _o[^1] : double.NaN; } - public double LastHigh { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _h.Count > 0 ? _h[^1] : double.NaN; } - public double LastLow { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _l.Count > 0 ? _l[^1] : double.NaN; } - public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; } - public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(TBar bar, bool isNew = true) - { - if (isNew || _c.Count == 0) - { - _t.Add(bar.Time); - _o.Add(bar.Open); - _h.Add(bar.High); - _l.Add(bar.Low); - _c.Add(bar.Close); - _v.Add(bar.Volume); - } - else - { - int lastIdx = _c.Count - 1; - _t[lastIdx] = bar.Time; - _o[lastIdx] = bar.Open; - _h[lastIdx] = bar.High; - _l[lastIdx] = bar.Low; - _c[lastIdx] = bar.Close; - _v[lastIdx] = bar.Volume; - } - - Pub?.Invoke(bar); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(long time, double open, double high, double low, double close, double volume, bool isNew = true) => - Add(new TBar(time, open, high, low, close, volume), isNew); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) => - Add(new TBar(time.Ticks, open, high, low, close, volume), isNew); - - public void Add(IEnumerable t, IEnumerable o, IEnumerable h, IEnumerable l, IEnumerable c, IEnumerable v) - { - var tArr = t as long[] ?? t.ToArray(); - var oArr = o as double[] ?? o.ToArray(); - var hArr = h as double[] ?? h.ToArray(); - var lArr = l as double[] ?? l.ToArray(); - var cArr = c as double[] ?? c.ToArray(); - var vArr = v as double[] ?? v.ToArray(); - - if (tArr.Length != oArr.Length || oArr.Length != hArr.Length || - hArr.Length != lArr.Length || lArr.Length != cArr.Length || - cArr.Length != vArr.Length) - { - throw new ArgumentException("All arrays must have the same length"); - } - - for (int i = 0; i < tArr.Length; i++) - { - Add(tArr[i], oArr[i], hArr[i], lArr[i], cArr[i], vArr[i]); - } - } - - public IEnumerator GetEnumerator() - { - for (int i = 0; i < _c.Count; i++) - { - yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} +using System.Collections; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace QuanTAlib; + +/// +/// A high-performance OHLCV time series implementation using Structure of Arrays (SoA) layout. +/// Stores Time, Open, High, Low, Close, Volume in separate contiguous arrays for SIMD efficiency. +/// Exposes TSeries views for each component that share the underlying Time array. +/// +public class TBarSeries : IReadOnlyList +{ + protected readonly List _t; + protected readonly List _o; + protected readonly List _h; + protected readonly List _l; + protected readonly List _c; + protected readonly List _v; + + public string Name { get; set; } = "Bar"; + public event Action? Pub; + + // Note: These views share underlying storage. Do not modify directly; use TBarSeries.Add() instead. + public TSeries Open { get; } + public TSeries High { get; } + public TSeries Low { get; } + public TSeries Close { get; } + public TSeries Volume { get; } + // Aliases for convenience + public TSeries O => Open; + public TSeries H => High; + public TSeries L => Low; + public TSeries C => Close; + public TSeries V => Volume; + + public TBarSeries() : this(0) + { + } + + public TBarSeries(int capacity) + { + _t = new List(capacity); + _o = new List(capacity); + _h = new List(capacity); + _l = new List(capacity); + _c = new List(capacity); + _v = new List(capacity); + + Open = new TSeries(_t, _o) { Name = "Open" }; + High = new TSeries(_t, _h) { Name = "High" }; + Low = new TSeries(_t, _l) { Name = "Low" }; + Close = new TSeries(_t, _c) { Name = "Close" }; + Volume = new TSeries(_t, _v) { Name = "Volume" }; + } + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _c.Count; + } + + public TBar this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(_t[index], _o[index], _h[index], _l[index], _c[index], _v[index]); + } + + public TBar Last + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _c.Count > 0 ? new(_t[^1], _o[^1], _h[^1], _l[^1], _c[^1], _v[^1]) : default; + } + + public long LastTime { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _t.Count > 0 ? _t[^1] : 0; } + public double LastOpen { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _o.Count > 0 ? _o[^1] : double.NaN; } + public double LastHigh { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _h.Count > 0 ? _h[^1] : double.NaN; } + public double LastLow { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _l.Count > 0 ? _l[^1] : double.NaN; } + public double LastClose { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _c.Count > 0 ? _c[^1] : double.NaN; } + public double LastVolume { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _v.Count > 0 ? _v[^1] : double.NaN; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TBar bar, bool isNew = true) + { + if (isNew || _c.Count == 0) + { + _t.Add(bar.Time); + _o.Add(bar.Open); + _h.Add(bar.High); + _l.Add(bar.Low); + _c.Add(bar.Close); + _v.Add(bar.Volume); + } + else + { + int lastIdx = _c.Count - 1; + _t[lastIdx] = bar.Time; + _o[lastIdx] = bar.Open; + _h[lastIdx] = bar.High; + _l[lastIdx] = bar.Low; + _c[lastIdx] = bar.Close; + _v[lastIdx] = bar.Volume; + } + + Pub?.Invoke(bar); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(long time, double open, double high, double low, double close, double volume, bool isNew = true) => + Add(new TBar(time, open, high, low, close, volume), isNew); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) => + Add(new TBar(time.Ticks, open, high, low, close, volume), isNew); + + public void Add(IEnumerable t, IEnumerable o, IEnumerable h, IEnumerable l, IEnumerable c, IEnumerable v) + { + var tArr = t as long[] ?? t.ToArray(); + var oArr = o as double[] ?? o.ToArray(); + var hArr = h as double[] ?? h.ToArray(); + var lArr = l as double[] ?? l.ToArray(); + var cArr = c as double[] ?? c.ToArray(); + var vArr = v as double[] ?? v.ToArray(); + + if (tArr.Length != oArr.Length || oArr.Length != hArr.Length || + hArr.Length != lArr.Length || lArr.Length != cArr.Length || + cArr.Length != vArr.Length) + { + throw new ArgumentException("All arrays must have the same length"); + } + + for (int i = 0; i < tArr.Length; i++) + { + Add(tArr[i], oArr[i], hArr[i], lArr[i], cArr[i], vArr[i]); + } + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _c.Count; i++) + { + yield return new TBar(_t[i], _o[i], _h[i], _l[i], _c[i], _v[i]); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/lib/core/tseries/tseries.cs b/lib/core/tseries/tseries.cs index 72f69511..6489f90a 100644 --- a/lib/core/tseries/tseries.cs +++ b/lib/core/tseries/tseries.cs @@ -1,134 +1,134 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace QuanTAlib; - -/// -/// A high-performance time series implementation using Structure of Arrays (SoA) layout. -/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency. -/// Supports "New Bar" vs "Update Last" streaming semantics. -/// - public class TSeries : IReadOnlyList, ITValuePublisher - { - protected readonly List _t; - protected readonly List _v; - - public string Name { get; set; } = "Data"; - - public event Action? Pub; - - public TSeries() : this(0) - { - } - - public TSeries(int capacity) - { - _t = new List(capacity); - _v = new List(capacity); - } - - public TSeries(List time, List values) - { - _t = time; - _v = values; - } - - public int Count - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _v.Count; - } - - public TValue this[int index] - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(_t[index], _v[index]); - } - - public TValue Last - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default; - } - - public double LastValue - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _v.Count > 0 ? _v[^1] : double.NaN; - } - - public long LastTime - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => _t.Count > 0 ? _t[^1] : 0; - } - - /// - /// Direct access to the underlying Value array as a Span for SIMD operations. - /// - public ReadOnlySpan Values - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => CollectionsMarshal.AsSpan(_v); - } - - /// - /// Direct access to the underlying Time array as a Span. - /// - public ReadOnlySpan Times - { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => CollectionsMarshal.AsSpan(_t); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual void Add(TValue value, bool isNew) - { - if (isNew || _v.Count == 0) - { - _t.Add(value.Time); - _v.Add(value.Value); - } - else - { - int lastIdx = _v.Count - 1; - _t[lastIdx] = value.Time; - _v[lastIdx] = value.Value; - } - Pub?.Invoke(value); - } - - // Overload for backward compatibility (assumes isNew=true) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public virtual void Add(TValue value) => Add(value, true); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew); - - public void Add(IEnumerable values) - { - long t = DateTime.UtcNow.Ticks; - foreach (var v in values) - { - Add(new TValue(t, v), isNew: true); - t += TimeSpan.TicksPerMinute; - } - } - - // IEnumerable implementation - public IEnumerator GetEnumerator() - { - for (int i = 0; i < _v.Count; i++) - { - yield return new TValue(_t[i], _v[i]); - } - } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); -} +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace QuanTAlib; + +/// +/// A high-performance time series implementation using Structure of Arrays (SoA) layout. +/// Stores Time (long) and Value (double) in separate contiguous arrays for SIMD efficiency. +/// Supports "New Bar" vs "Update Last" streaming semantics. +/// +public class TSeries : IReadOnlyList, ITValuePublisher +{ + protected readonly List _t; + protected readonly List _v; + + public string Name { get; set; } = "Data"; + + public event Action? Pub; + + public TSeries() : this(0) + { + } + + public TSeries(int capacity) + { + _t = new List(capacity); + _v = new List(capacity); + } + + public TSeries(List time, List values) + { + _t = time; + _v = values; + } + + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _v.Count; + } + + public TValue this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(_t[index], _v[index]); + } + + public TValue Last + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _v.Count > 0 ? new(_t[^1], _v[^1]) : default; + } + + public double LastValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _v.Count > 0 ? _v[^1] : double.NaN; + } + + public long LastTime + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _t.Count > 0 ? _t[^1] : 0; + } + + /// + /// Direct access to the underlying Value array as a Span for SIMD operations. + /// + public ReadOnlySpan Values + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => CollectionsMarshal.AsSpan(_v); + } + + /// + /// Direct access to the underlying Time array as a Span. + /// + public ReadOnlySpan Times + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => CollectionsMarshal.AsSpan(_t); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(TValue value, bool isNew) + { + if (isNew || _v.Count == 0) + { + _t.Add(value.Time); + _v.Add(value.Value); + } + else + { + int lastIdx = _v.Count - 1; + _t[lastIdx] = value.Time; + _v[lastIdx] = value.Value; + } + Pub?.Invoke(value); + } + + // Overload for backward compatibility (assumes isNew=true) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public virtual void Add(TValue value) => Add(value, true); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(long time, double value, bool isNew = true) => Add(new TValue(time, value), isNew); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(DateTime time, double value, bool isNew = true) => Add(new TValue(time, value), isNew); + + public void Add(IEnumerable values) + { + long t = DateTime.UtcNow.Ticks; + foreach (var v in values) + { + Add(new TValue(t, v), isNew: true); + t += TimeSpan.TicksPerMinute; + } + } + + // IEnumerable implementation + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _v.Count; i++) + { + yield return new TValue(_t[i], _v[i]); + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/lib/core/tvalue/tvalue.cs b/lib/core/tvalue/tvalue.cs index c38fe930..9706ae64 100644 --- a/lib/core/tvalue/tvalue.cs +++ b/lib/core/tvalue/tvalue.cs @@ -1,47 +1,47 @@ -using System.Runtime.CompilerServices; - -namespace QuanTAlib; - -/// -/// A lightweight struct representing a time-value pair. -/// Pure data type: 16 bytes (long + double). -/// -[SkipLocalsInit] -public readonly struct TValue : IEquatable -{ - public readonly long Time; - public readonly double Value; - - public DateTime AsDateTime => new(Time, DateTimeKind.Utc); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue(long time, double value) - { - Time = time; - Value = value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue(DateTime time, double value) - { - Time = time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks; - Value = value; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator double(TValue tv) => tv.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]"; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(TValue other) => Time == other.Time && Value == other.Value; - - public override bool Equals(object? obj) => obj is TValue other && Equals(other); - public override int GetHashCode() => HashCode.Combine(Time, Value); - public static bool operator ==(TValue left, TValue right) => left.Equals(right); - public static bool operator !=(TValue left, TValue right) => !left.Equals(right); -} +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// A lightweight struct representing a time-value pair. +/// Pure data type: 16 bytes (long + double). +/// +[SkipLocalsInit] +public readonly struct TValue : IEquatable +{ + public readonly long Time; + public readonly double Value; + + public DateTime AsDateTime => new(Time, DateTimeKind.Utc); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue(long time, double value) + { + Time = time; + Value = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue(DateTime time, double value) + { + Time = time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks; + Value = value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator double(TValue tv) => tv.Value; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator DateTime(TValue tv) => new(tv.Time, DateTimeKind.Utc); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override string ToString() => $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {Value:F2}]"; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool Equals(TValue other) => Time == other.Time && Value == other.Value; + + public override bool Equals(object? obj) => obj is TValue other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Time, Value); + public static bool operator ==(TValue left, TValue right) => left.Equals(right); + public static bool operator !=(TValue left, TValue right) => !left.Equals(right); +} diff --git a/lib/momentum/adx/Adx.Quantower.Tests.cs b/lib/momentum/adx/Adx.Quantower.Tests.cs index 3ad45ea3..b7f15613 100644 --- a/lib/momentum/adx/Adx.Quantower.Tests.cs +++ b/lib/momentum/adx/Adx.Quantower.Tests.cs @@ -56,7 +56,7 @@ public class AdxIndicatorTests indicator.Initialize(); // After init, line series should exist (ADX, +DI, -DI) - Assert.Equal(3, indicator.LinesSeries.Length); + Assert.Equal(3, indicator.LinesSeries.Count); } [Fact] @@ -71,7 +71,7 @@ public class AdxIndicatorTests for (int i = 0; i < 20; i++) { indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); - + // Process update for each bar to simulate history loading var args = new UpdateArgs(UpdateReason.HistoricalBar); indicator.ProcessUpdate(args); @@ -79,7 +79,7 @@ public class AdxIndicatorTests // Line series should have a value double adx = indicator.LinesSeries[0].GetValue(0); - + Assert.True(double.IsFinite(adx)); } } diff --git a/lib/momentum/adx/Adx.Quantower.cs b/lib/momentum/adx/Adx.Quantower.cs index 3b643cda..9ae1f4c0 100644 --- a/lib/momentum/adx/Adx.Quantower.cs +++ b/lib/momentum/adx/Adx.Quantower.cs @@ -28,11 +28,11 @@ public class AdxIndicator : Indicator, IWatchlistIndicator SeparateWindow = true; Name = "ADX - Average Directional Index"; Description = "Measures the strength of a trend"; - + AdxSeries = new(name: "ADX", color: Color.Blue, width: 2, style: LineStyle.Solid); DiPlusSeries = new(name: "+DI", color: Color.Green, width: 1, style: LineStyle.Solid); DiMinusSeries = new(name: "-DI", color: Color.Red, width: 1, style: LineStyle.Solid); - + AddLineSeries(AdxSeries); AddLineSeries(DiPlusSeries); AddLineSeries(DiMinusSeries); @@ -51,7 +51,7 @@ public class AdxIndicator : Indicator, IWatchlistIndicator TBar bar = this.GetInputBar(args); TValue result = _adx!.Update(bar, isNew); - + if (!_adx.IsHot && !ShowColdValues) { return; diff --git a/lib/momentum/adx/Adx.Tests.cs b/lib/momentum/adx/Adx.Tests.cs index a25b7e5b..0577778b 100644 --- a/lib/momentum/adx/Adx.Tests.cs +++ b/lib/momentum/adx/Adx.Tests.cs @@ -115,7 +115,7 @@ public class AdxTests streamingResults.Add(adx.Update(bars[i]).Value); } - var staticResults = Adx.Calculate(bars, 14); + var staticResults = Adx.Batch(bars, 14); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < staticResults.Count; i++) diff --git a/lib/momentum/adx/Adx.cs b/lib/momentum/adx/Adx.cs index b9577be7..e02874ce 100644 --- a/lib/momentum/adx/Adx.cs +++ b/lib/momentum/adx/Adx.cs @@ -38,7 +38,7 @@ public sealed class Adx : ITValuePublisher private double _p_trSum, _p_dmPlusSum, _p_dmMinusSum; private int _samples; private int _p_samples; - + private double _trSmooth, _dmPlusSmooth, _dmMinusSmooth; private double _p_trSmooth, _p_dmPlusSmooth, _p_dmMinusSmooth; @@ -47,7 +47,7 @@ public sealed class Adx : ITValuePublisher private double _p_dxSum; private int _dxSamples; private int _p_dxSamples; - + private double _adx; private double _p_adx; @@ -78,6 +78,11 @@ public sealed class Adx : ITValuePublisher /// public bool IsHot => _dxSamples >= _period; + /// + /// The number of bars required for the indicator to warm up. + /// + public int WarmupPeriod { get; } + /// /// Creates ADX with specified period. /// @@ -89,6 +94,7 @@ public sealed class Adx : ITValuePublisher _period = period; Name = $"Adx({period})"; + WarmupPeriod = period * 2; // Needs period for TR/DM smoothing, then period for ADX smoothing _isInitialized = false; } @@ -101,19 +107,19 @@ public sealed class Adx : ITValuePublisher _prevBar = default; _p_prevBar = default; _isInitialized = false; - + _trSum = _dmPlusSum = _dmMinusSum = 0; _p_trSum = _p_dmPlusSum = _p_dmMinusSum = 0; _samples = _p_samples = 0; - + _trSmooth = _dmPlusSmooth = _dmMinusSmooth = 0; _p_trSmooth = _p_dmPlusSmooth = _p_dmMinusSmooth = 0; - + _dxSum = _p_dxSum = 0; _dxSamples = _p_dxSamples = 0; - + _adx = _p_adx = 0; - + Last = default; DiPlus = default; DiMinus = default; @@ -175,7 +181,7 @@ public sealed class Adx : ITValuePublisher if (upMove > downMove && upMove > 0) dmPlus = upMove; - + if (downMove > upMove && downMove > 0) dmMinus = downMove; @@ -211,7 +217,7 @@ public sealed class Adx : ITValuePublisher // Wilder uses sums, but effectively it's RMA. // Standard formula: // Smooth = Smooth - (Smooth / Period) + Input - + _trSmooth = _trSmooth - (_trSmooth / _period) + tr; _dmPlusSmooth = _dmPlusSmooth - (_dmPlusSmooth / _period) + dmPlus; _dmMinusSmooth = _dmMinusSmooth - (_dmMinusSmooth / _period) + dmMinus; @@ -235,13 +241,13 @@ public sealed class Adx : ITValuePublisher { dx = (Math.Abs(diPlus - diMinus) / diSum) * 100.0; } - + // Smooth DX to get ADX if (_dxSamples < _period) { _dxSum += dx; _dxSamples++; - + if (_dxSamples == _period) { _adx = _dxSum / _period; // First ADX is SMA of DX @@ -257,7 +263,7 @@ public sealed class Adx : ITValuePublisher DiPlus = new TValue(input.Time, diPlus); DiMinus = new TValue(input.Time, diMinus); Last = new TValue(input.Time, _adx); - + Pub?.Invoke(Last); return Last; } @@ -284,7 +290,7 @@ public sealed class Adx : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int period) + public static TSeries Batch(TBarSeries source, int period) { var adx = new Adx(period); return adx.Update(source); diff --git a/lib/momentum/adx/Adx.md b/lib/momentum/adx/Adx.md index ff94968c..596275a7 100644 --- a/lib/momentum/adx/Adx.md +++ b/lib/momentum/adx/Adx.md @@ -59,10 +59,10 @@ var series = new TBarSeries(); var results = adx.Update(series); ``` -### Static Calculation +### Batch Calculation ```csharp -var results = Adx.Calculate(series, 14); +var results = Adx.Batch(series, 14); ``` ## Interpretation diff --git a/lib/momentum/ao/Ao.Quantower.Tests.cs b/lib/momentum/ao/Ao.Quantower.Tests.cs index c421d936..1c7bed4b 100644 --- a/lib/momentum/ao/Ao.Quantower.Tests.cs +++ b/lib/momentum/ao/Ao.Quantower.Tests.cs @@ -58,7 +58,7 @@ public class AoIndicatorTests indicator.Initialize(); // After init, line series should exist (Up and Down) - Assert.Equal(2, indicator.LinesSeries.Length); + Assert.Equal(2, indicator.LinesSeries.Count); } [Fact] @@ -73,7 +73,7 @@ public class AoIndicatorTests for (int i = 0; i < 20; i++) { indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); - + // Process update for each bar to simulate history loading var args = new UpdateArgs(UpdateReason.HistoricalBar); indicator.ProcessUpdate(args); @@ -83,7 +83,7 @@ public class AoIndicatorTests // One should be NaN, other should be value, or both NaN if cold double up = indicator.LinesSeries[0].GetValue(0); double down = indicator.LinesSeries[1].GetValue(0); - + Assert.True(double.IsFinite(up) || double.IsFinite(down)); } @@ -100,7 +100,7 @@ public class AoIndicatorTests } indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); - + // Add new bar indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125); indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); diff --git a/lib/momentum/ao/Ao.Quantower.cs b/lib/momentum/ao/Ao.Quantower.cs index 029abe88..1683b97e 100644 --- a/lib/momentum/ao/Ao.Quantower.cs +++ b/lib/momentum/ao/Ao.Quantower.cs @@ -71,7 +71,7 @@ public class AoIndicator : Indicator, IWatchlistIndicator // or just use _ao.Last (which is current) and we need the previous one. // But _ao doesn't expose history directly unless we use TSeries. // However, Quantower stores history in the Series. - + // Get previous value from series double prevAo = double.NaN; if (Count > 1) diff --git a/lib/momentum/ao/Ao.Tests.cs b/lib/momentum/ao/Ao.Tests.cs index b2de29ca..b2c4fea6 100644 --- a/lib/momentum/ao/Ao.Tests.cs +++ b/lib/momentum/ao/Ao.Tests.cs @@ -101,7 +101,7 @@ public class AoTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -113,7 +113,7 @@ public class AoTests streamingResults.Add(ao.Update(bars[i]).Value); } - var staticResults = Ao.Calculate(bars, 5, 34); + var staticResults = Ao.Batch(bars, 5, 34); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < staticResults.Count; i++) diff --git a/lib/momentum/ao/Ao.cs b/lib/momentum/ao/Ao.cs index b08fcd73..8aabe8bd 100644 --- a/lib/momentum/ao/Ao.cs +++ b/lib/momentum/ao/Ao.cs @@ -41,6 +41,11 @@ public sealed class Ao : ITValuePublisher /// public bool IsHot => _smaSlow.IsHot; + /// + /// The number of bars required to warm up the indicator. + /// + public int WarmupPeriod { get; } + /// /// Creates AO with specified periods. /// @@ -57,6 +62,7 @@ public sealed class Ao : ITValuePublisher _smaFast = new Sma(fastPeriod); _smaSlow = new Sma(slowPeriod); + WarmupPeriod = slowPeriod; Name = $"Ao({fastPeriod},{slowPeriod})"; } @@ -139,7 +145,7 @@ public sealed class Ao : ITValuePublisher /// Fast SMA period (default 5) /// Slow SMA period (default 34) /// AO series - public static TSeries Calculate(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34) + public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34) { var ao = new Ao(fastPeriod, slowPeriod); return ao.Update(source); diff --git a/lib/momentum/ao/Ao.md b/lib/momentum/ao/Ao.md index 81988592..f8149b8c 100644 --- a/lib/momentum/ao/Ao.md +++ b/lib/momentum/ao/Ao.md @@ -30,6 +30,10 @@ var result = ao.Update(bar); // Result contains the AO value Console.WriteLine($"AO: {result.Value}"); + +// Batch calculation +var series = new TBarSeries(); +var results = Ao.Batch(series, 5, 34); ``` ### Parameters diff --git a/lib/momentum/cfb/Cfb.Quantower.Tests.cs b/lib/momentum/cfb/Cfb.Quantower.Tests.cs index 2a405258..322cf61c 100644 --- a/lib/momentum/cfb/Cfb.Quantower.Tests.cs +++ b/lib/momentum/cfb/Cfb.Quantower.Tests.cs @@ -101,7 +101,7 @@ public class CfbIndicatorTests indicator.HistoricalData.AddBar(now.AddMinutes(3), 103, 109, 101, 105); indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); - + // Add new bar indicator.HistoricalData.AddBar(now.AddMinutes(4), 105, 112, 103, 110); indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); @@ -137,7 +137,7 @@ public class CfbIndicatorTests { var indicator = new CfbIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(CfbIndicator), method.DeclaringType); @@ -159,7 +159,7 @@ public class CfbIndicatorTests { indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); } - + indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)), diff --git a/lib/momentum/cfb/Cfb.Tests.cs b/lib/momentum/cfb/Cfb.Tests.cs index 494efa61..b63ea013 100644 --- a/lib/momentum/cfb/Cfb.Tests.cs +++ b/lib/momentum/cfb/Cfb.Tests.cs @@ -111,7 +111,7 @@ public class CfbTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -124,7 +124,7 @@ public class CfbTests streamingResults.Add(cfb.Update(new TValue(series.Times[i], series.Values[i])).Value); } - var staticResults = Cfb.Calculate(series); + var staticResults = Cfb.Batch(series); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < streamingResults.Count; i++) @@ -134,7 +134,7 @@ public class CfbTests } [Fact] - public void SpanCalculate_Matches_Streaming() + public void SpanBatch_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -148,7 +148,7 @@ public class CfbTests } double[] spanResults = new double[bars.Count]; - Cfb.Calculate(values, spanResults); + Cfb.Batch(values, spanResults); for (int i = 0; i < streamingResults.Count; i++) { diff --git a/lib/momentum/cfb/Cfb.Validation.Tests.cs b/lib/momentum/cfb/Cfb.Validation.Tests.cs index aedbb10f..bfb979a6 100644 --- a/lib/momentum/cfb/Cfb.Validation.Tests.cs +++ b/lib/momentum/cfb/Cfb.Validation.Tests.cs @@ -16,9 +16,9 @@ public class CfbValidationTests } [Fact] - public void Validate_Consistency_UpdateVsCalculate() + public void Validate_Consistency_UpdateVsBatch() { - // Verify that Update(TValue) and Calculate(TSeries) produce identical results + // Verify that Update(TValue) and Batch(TSeries) produce identical results var cfb = new Cfb(); var streamResult = new TSeries(); foreach (var item in _testData.Data) @@ -26,7 +26,7 @@ public class CfbValidationTests streamResult.Add(cfb.Update(item)); } - var batchResult = Cfb.Calculate(_testData.Data); + var batchResult = Cfb.Batch(_testData.Data); Assert.Equal(streamResult.Count, batchResult.Count); Assert.NotEmpty(streamResult); @@ -34,18 +34,18 @@ public class CfbValidationTests { Assert.Equal(streamResult[i].Value, batchResult[i].Value, 1e-9); } - _output.WriteLine("CFB Update vs Calculate validated successfully"); + _output.WriteLine("CFB Update vs Batch validated successfully"); } [Fact] public void Validate_Consistency_SeriesVsSpan() { - // Verify that Calculate(TSeries) and Calculate(Span) produce identical results - var batchResult = Cfb.Calculate(_testData.Data); + // Verify that Batch(TSeries) and Batch(Span) produce identical results + var batchResult = Cfb.Batch(_testData.Data); var spanInput = _testData.Data.Values.ToArray().AsSpan(); var spanOutput = new double[spanInput.Length]; - Cfb.Calculate(spanInput, spanOutput); + Cfb.Batch(spanInput, spanOutput); for (int i = 0; i < batchResult.Count; i++) { @@ -58,7 +58,7 @@ public class CfbValidationTests public void Validate_Properties() { // CFB should be >= 1.0 - var result = Cfb.Calculate(_testData.Data); + var result = Cfb.Batch(_testData.Data); foreach (var val in result.Values) { Assert.True(val >= 1.0, $"CFB value {val} should be >= 1.0"); diff --git a/lib/momentum/cfb/Cfb.cs b/lib/momentum/cfb/Cfb.cs index 4f9143e6..c2af3bb1 100644 --- a/lib/momentum/cfb/Cfb.cs +++ b/lib/momentum/cfb/Cfb.cs @@ -45,6 +45,7 @@ public sealed class Cfb : ITValuePublisher public event Action? Pub; public TValue Last { get; private set; } public bool IsHot => _prices.IsFull; + public int WarmupPeriod { get; } /// /// Creates a CFB indicator with specified fractal lengths. @@ -68,16 +69,17 @@ public sealed class Cfb : ITValuePublisher } _maxLen = _lengths[^1]; - + WarmupPeriod = _maxLen; + // We need maxLen + 1 capacity to handle the lookback correctly // _prices stores raw prices // _volatility stores bar-to-bar changes. _volatility[i] = Abs(Price[i] - Price[i-1]) _prices = new RingBuffer(_maxLen + 1); _volatility = new RingBuffer(_maxLen + 1); - + _runningSums = new double[_lengths.Length]; _p_runningSums = new double[_lengths.Length]; - + Name = "Cfb"; _state.PrevCfb = 1.0; } @@ -155,17 +157,17 @@ public sealed class Cfb : ITValuePublisher for (int i = 0; i < _lengths.Length; i++) { int L = _lengths[i]; - + // Update running sum of volatility // We always add the new volatility // We only subtract if we have enough history - + double volToRemove = 0.0; if (count > L) { volToRemove = _volatility[count - 1 - L]; } - + _runningSums[i] += vol - volToRemove; if (count <= L) continue; @@ -176,7 +178,7 @@ public sealed class Cfb : ITValuePublisher // Net move over L bars // Price at Count-1 is current. Price at Count-1-L is L bars ago. double netMove = Math.Abs(price - _prices[count - 1 - L]); - + double ratio = netMove / _runningSums[i]; @@ -199,7 +201,7 @@ public sealed class Cfb : ITValuePublisher } if (cfb < 1.0) cfb = 1.0; - + // Round to nearest integer cfb = Math.Round(cfb); if (cfb < 1.0) cfb = 1.0; @@ -224,14 +226,14 @@ public sealed class Cfb : ITValuePublisher var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _lengths); + Batch(source.Values, vSpan, _lengths); source.Times.CopyTo(tSpan); // Restore state logic would go here if needed for continuity, // but for batch processing we usually just return the result. // To properly support "Update(TValue)" after "Update(TSeries)", we would need to // replay the last MaxLen bars to populate the buffers. - + // Replay last MaxLen bars to restore state int replayStart = Math.Max(0, len - _maxLen - 1); _prices.Clear(); @@ -243,7 +245,7 @@ public sealed class Cfb : ITValuePublisher // We need to re-run the update logic for the replay window to populate running sums correctly // This is expensive but necessary for correct state restoration. // For the purpose of this implementation, we will just ensure the buffers are populated. - + for (int i = replayStart; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i]), true); @@ -252,14 +254,14 @@ public sealed class Cfb : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int[]? lengths = null) + public static TSeries Batch(TSeries source, int[]? lengths = null) { var cfb = new Cfb(lengths); return cfb.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int[]? lengths = null) + public static void Batch(ReadOnlySpan source, Span output, int[]? lengths = null) { if (source.Length == 0) return; @@ -275,7 +277,7 @@ public sealed class Cfb : ITValuePublisher lens = lengths; } int maxLen = 0; - for(int i=0; i maxLen) maxLen = lens[i]; + for (int i = 0; i < lens.Length; i++) if (lens[i] > maxLen) maxLen = lens[i]; // Pre-calculate volatility for the whole series // vol[i] = Abs(source[i] - source[i-1]) @@ -285,7 +287,7 @@ public sealed class Cfb : ITValuePublisher volArray[0] = 0; for (int i = 1; i < len; i++) { - volArray[i] = Math.Abs(source[i] - source[i-1]); + volArray[i] = Math.Abs(source[i] - source[i - 1]); } // We need running sums for each length. @@ -297,7 +299,7 @@ public sealed class Cfb : ITValuePublisher { double price = source[i]; double currentVol = volArray[i]; - + double sumWeightedLen = 0.0; double sumWeights = 0.0; @@ -317,14 +319,14 @@ public sealed class Cfb : ITValuePublisher for (int k = 0; k < lens.Length; k++) { int L = lens[k]; - + // Update running sum runningSums[k] += currentVol; if (i > L) { runningSums[k] -= volArray[i - L]; } - + if (i < L) continue; double totalMove = runningSums[k]; diff --git a/lib/momentum/cfb/Cfb.md b/lib/momentum/cfb/Cfb.md index efbf1e5d..fa90977b 100644 --- a/lib/momentum/cfb/Cfb.md +++ b/lib/momentum/cfb/Cfb.md @@ -74,7 +74,7 @@ double[] prices = ...; double[] output = new double[prices.Length]; // Calculate using default lengths -Cfb.Calculate(prices.AsSpan(), output.AsSpan()); +Cfb.Batch(prices.AsSpan(), output.AsSpan()); ``` ### Bar Correction (isNew Parameter) diff --git a/lib/momentum/dmx/Dmx.Quantower.Tests.cs b/lib/momentum/dmx/Dmx.Quantower.Tests.cs index b0337224..da2116c7 100644 --- a/lib/momentum/dmx/Dmx.Quantower.Tests.cs +++ b/lib/momentum/dmx/Dmx.Quantower.Tests.cs @@ -96,7 +96,7 @@ public class DmxIndicatorTests } indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); - + // Add new bar indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115); indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); @@ -131,7 +131,7 @@ public class DmxIndicatorTests { var indicator = new DmxIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(DmxIndicator), method.DeclaringType); diff --git a/lib/momentum/dmx/Dmx.Tests.cs b/lib/momentum/dmx/Dmx.Tests.cs index ed773b67..1c5447f6 100644 --- a/lib/momentum/dmx/Dmx.Tests.cs +++ b/lib/momentum/dmx/Dmx.Tests.cs @@ -112,7 +112,7 @@ public class DmxTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -124,7 +124,7 @@ public class DmxTests streamingResults.Add(dmx.Update(bars[i]).Value); } - var staticResults = Dmx.Calculate(bars, 14); + var staticResults = Dmx.Batch(bars, 14); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < streamingResults.Count; i++) diff --git a/lib/momentum/dmx/Dmx.cs b/lib/momentum/dmx/Dmx.cs index a56686c0..dd532fdf 100644 --- a/lib/momentum/dmx/Dmx.cs +++ b/lib/momentum/dmx/Dmx.cs @@ -23,10 +23,12 @@ public sealed class Dmx : ITValuePublisher public string Name { get; } public event Action? Pub; public TValue Last { get; private set; } + public int WarmupPeriod { get; } public Dmx(int period) { Name = $"Dmx({period})"; + WarmupPeriod = period; _jmaDMp = new Jma(period); _jmaDMm = new Jma(period); _jmaTR = new Jma(period); @@ -61,7 +63,7 @@ public sealed class Dmx : ITValuePublisher // But we want to handle the first bar logic specifically } } - + // We always update _lastInput to the current input _lastInput = input; @@ -80,14 +82,14 @@ public sealed class Dmx : ITValuePublisher if (upMove > downMove && upMove > 0) dmPlusRaw = upMove; - + if (downMove > upMove && downMove > 0) dmMinusRaw = downMove; double tr1 = input.High - input.Low; double tr2 = Math.Abs(input.High - _prevBar.Close); double tr3 = Math.Abs(input.Low - _prevBar.Close); - + trRaw = Math.Max(tr1, Math.Max(tr2, tr3)); } @@ -130,7 +132,7 @@ public sealed class Dmx : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int period = 14) + public static TSeries Batch(TBarSeries source, int period = 14) { var dmx = new Dmx(period); return dmx.Update(source); diff --git a/lib/momentum/dmx/Dmx.md b/lib/momentum/dmx/Dmx.md index 4d0abbb6..1cf48c64 100644 --- a/lib/momentum/dmx/Dmx.md +++ b/lib/momentum/dmx/Dmx.md @@ -85,8 +85,7 @@ foreach(var bar in bars) { ### Batch Processing ```csharp -var dmx = new Dmx(14); -var resultSeries = dmx.Update(bars); +var resultSeries = Dmx.Batch(bars, 14); ``` ## Interpretation diff --git a/lib/momentum/rsx/Rsx.Quantower.Tests.cs b/lib/momentum/rsx/Rsx.Quantower.Tests.cs index 41970172..3f6acc20 100644 --- a/lib/momentum/rsx/Rsx.Quantower.Tests.cs +++ b/lib/momentum/rsx/Rsx.Quantower.Tests.cs @@ -118,7 +118,7 @@ public class RsxIndicatorTests { var indicator = new RsxIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(RsxIndicator), method.DeclaringType); diff --git a/lib/momentum/rsx/Rsx.Tests.cs b/lib/momentum/rsx/Rsx.Tests.cs index 4b23ad7c..2dc35f11 100644 --- a/lib/momentum/rsx/Rsx.Tests.cs +++ b/lib/momentum/rsx/Rsx.Tests.cs @@ -58,7 +58,7 @@ public class RsxTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { int period = 14; int count = 100; @@ -72,7 +72,7 @@ public class RsxTests streamingResults.Add(rsx.Update(new TValue(series.Times[i], series.Values[i])).Value); } - var staticResults = Rsx.Calculate(series, period); + var staticResults = Rsx.Batch(series, period); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < count; i++) @@ -82,7 +82,7 @@ public class RsxTests } [Fact] - public void SpanCalculate_Matches_Streaming() + public void SpanBatch_Matches_Streaming() { int period = 14; int count = 100; @@ -98,7 +98,7 @@ public class RsxTests var spanInput = series.Values.ToArray(); var spanOutput = new double[count]; - Rsx.Calculate(spanInput, spanOutput, period); + Rsx.Batch(spanInput, spanOutput, period); for (int i = 0; i < count; i++) { diff --git a/lib/momentum/rsx/Rsx.cs b/lib/momentum/rsx/Rsx.cs index e3e53b93..97016fc3 100644 --- a/lib/momentum/rsx/Rsx.cs +++ b/lib/momentum/rsx/Rsx.cs @@ -54,6 +54,11 @@ public sealed class Rsx : ITValuePublisher public event Action? Pub; + /// + /// The number of bars required to warm up the indicator. + /// + public int WarmupPeriod { get; } + /// /// Creates RSX with specified period. /// @@ -64,6 +69,7 @@ public sealed class Rsx : ITValuePublisher throw new ArgumentException("Period must be greater than 0", nameof(period)); _period = period; + WarmupPeriod = period; _alpha = 3.0 / (period + 2.0); Name = $"Rsx({period})"; } @@ -113,10 +119,10 @@ public sealed class Rsx : ITValuePublisher // Calculate momentum (change in price * 100) double momentum = (price - _state.LastPrice) * 100.0; - + if (isNew) { - _state.LastPrice = price; + _state.LastPrice = price; } // --- Momentum Smoothing --- @@ -184,7 +190,7 @@ public sealed class Rsx : ITValuePublisher var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); // Restore state by replaying the last few bars @@ -199,14 +205,14 @@ public sealed class Rsx : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var rsx = new Rsx(period); return rsx.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); diff --git a/lib/momentum/rsx/Rsx.md b/lib/momentum/rsx/Rsx.md index 74e8ce7f..42c7adc7 100644 --- a/lib/momentum/rsx/Rsx.md +++ b/lib/momentum/rsx/Rsx.md @@ -45,7 +45,7 @@ Console.WriteLine($"RSX: {result.Value}"); double[] prices = { ... }; double[] results = new double[prices.Length]; -Rsx.Calculate(prices, results, 14); +Rsx.Batch(prices, results, 14); ``` ### Chaining diff --git a/lib/momentum/vel/Vel.Quantower.Tests.cs b/lib/momentum/vel/Vel.Quantower.Tests.cs index d891feb7..abcd6965 100644 --- a/lib/momentum/vel/Vel.Quantower.Tests.cs +++ b/lib/momentum/vel/Vel.Quantower.Tests.cs @@ -118,7 +118,7 @@ public class VelIndicatorTests { var indicator = new VelIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(VelIndicator), method.DeclaringType); diff --git a/lib/momentum/vel/Vel.Tests.cs b/lib/momentum/vel/Vel.Tests.cs index e53f8673..75de110c 100644 --- a/lib/momentum/vel/Vel.Tests.cs +++ b/lib/momentum/vel/Vel.Tests.cs @@ -106,14 +106,14 @@ public class VelTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { var series = new TSeries(); series.Add(DateTime.UtcNow.Ticks, 10); series.Add(DateTime.UtcNow.Ticks + 1, 20); series.Add(DateTime.UtcNow.Ticks + 2, 30); - var results = Vel.Calculate(series, 3); + var results = Vel.Batch(series, 3); Assert.Equal(3, results.Count); @@ -125,7 +125,7 @@ public class VelTests } [Fact] - public void SpanCalculate_Matches_Streaming() + public void SpanBatch_Matches_Streaming() { var series = new TSeries(); double[] source = new double[100]; @@ -140,10 +140,10 @@ public class VelTests } // Calculate with TSeries API - var tseriesResult = Vel.Calculate(series, 10); + var tseriesResult = Vel.Batch(series, 10); // Calculate with Span API - Vel.Calculate(source.AsSpan(), output.AsSpan(), 10); + Vel.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) diff --git a/lib/momentum/vel/Vel.cs b/lib/momentum/vel/Vel.cs index 4bf110e1..ece60723 100644 --- a/lib/momentum/vel/Vel.cs +++ b/lib/momentum/vel/Vel.cs @@ -25,14 +25,16 @@ public sealed class Vel : ITValuePublisher public string Name { get; } public TValue Last { get; private set; } public bool IsHot => _pwma.IsHot && _wma.IsHot; + public int WarmupPeriod { get; } public event Action? Pub; public Vel(int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); - + _pwma = new Pwma(period); _wma = new Wma(period); + WarmupPeriod = period; Name = $"Vel({period})"; } @@ -68,7 +70,7 @@ public sealed class Vel : ITValuePublisher CollectionsMarshal.SetCount(v, len); var vSpan = CollectionsMarshal.AsSpan(v); - + SimdExtensions.Subtract(pwmaSeries.Values, wmaSeries.Values, vSpan); source.Times.CopyTo(CollectionsMarshal.AsSpan(t)); @@ -76,14 +78,14 @@ public sealed class Vel : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var vel = new Vel(period); return vel.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); @@ -92,7 +94,7 @@ public sealed class Vel : ITValuePublisher Span wma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length]; Pwma.Calculate(source, pwma, period); - Wma.Calculate(source, wma, period); + Wma.Batch(source, wma, period); SimdExtensions.Subtract(pwma, wma, output); } diff --git a/lib/momentum/vel/Vel.md b/lib/momentum/vel/Vel.md index ebeba9e0..b69e4cb8 100644 --- a/lib/momentum/vel/Vel.md +++ b/lib/momentum/vel/Vel.md @@ -47,12 +47,12 @@ var vel = new Vel(source, 14); ### Batch Calculation (Span) -For high-performance scenarios, use the static `Calculate` method with `Span`. +For high-performance scenarios, use the static `Batch` method with `Span`. ```csharp double[] prices = { ... }; double[] results = new double[prices.Length]; -Vel.Calculate(prices, results, 14); +Vel.Batch(prices, results, 14); ``` ## Interpretation diff --git a/lib/trends/_index.md b/lib/trends/_index.md index fb31d3f8..93b2bf66 100644 --- a/lib/trends/_index.md +++ b/lib/trends/_index.md @@ -56,7 +56,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov | SINEMA | Sine-weighted MA | | | [SMA](sma/Sma.md) | Simple MA | The unweighted mean of the previous n data. | | SSF | Ehlers Super Smooth Filter | | -| SUPER | SuperTrend | | +| [SUPER](super/Super.md) | SuperTrend | Trend-following indicator using ATR to define upper and lower bands acting as a trailing stop. | | [T3](t3/T3.md) | Tillson T3 MA | A smooth moving average that uses a smoothing factor to reduce lag. | | [TEMA](tema/Tema.md) | Triple Exponential MA | Designed to smooth price fluctuations and filter out volatility. | | [TRIMA](trima/Trima.md) | Triangular MA | A double-smoothed SMA that gives more weight to the middle of the data window. | diff --git a/lib/trends/alma/Alma.Tests.cs b/lib/trends/alma/Alma.Tests.cs index b1184d94..eb8f0072 100644 --- a/lib/trends/alma/Alma.Tests.cs +++ b/lib/trends/alma/Alma.Tests.cs @@ -87,7 +87,7 @@ public class AlmaTests } var instanceResults = new Alma(10).Update(series); - var staticResults = Alma.Calculate(series, 10); + var staticResults = Alma.Batch(series, 10); for (int i = 0; i < instanceResults.Count; i++) { @@ -106,7 +106,7 @@ public class AlmaTests series.Add(bar.Time, bar.Close); } - var seriesResults = Alma.Calculate(series, 10); + var seriesResults = Alma.Batch(series, 10); double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; @@ -273,7 +273,7 @@ public class AlmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Alma.Calculate(series, period); + var batchSeries = Alma.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/alma/Alma.cs b/lib/trends/alma/Alma.cs index fdd7c0af..a385a365 100644 --- a/lib/trends/alma/Alma.cs +++ b/lib/trends/alma/Alma.cs @@ -23,7 +23,7 @@ namespace QuanTAlib; /// The final ALMA is the weighted sum of the price window divided by the sum of weights. /// [SkipLocalsInit] -public sealed class Alma : ITValuePublisher +public sealed class Alma : AbstractBase { private readonly int _period; private readonly double _offset; @@ -36,22 +36,7 @@ public sealed class Alma : ITValuePublisher private State _state; private State _p_state; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; - - /// - /// Current ALMA value. - /// - public TValue Last { get; private set; } - - /// - /// True if the ALMA has enough data to produce valid results (buffer is full). - /// - public bool IsHot => _buffer.IsFull; + public override bool IsHot => _buffer.IsFull; /// /// Creates ALMA with specified parameters. @@ -74,6 +59,7 @@ public sealed class Alma : ITValuePublisher _buffer = new RingBuffer(period); _weights = new double[period]; Name = $"Alma({period}, {offset:F2}, {sigma:F2})"; + WarmupPeriod = period; // Precompute weights double m = offset * (period - 1); @@ -91,7 +77,7 @@ public sealed class Alma : ITValuePublisher _weightSum = sum; } - public Alma(ITValuePublisher source, int period, double offset = 0.85, double sigma = 6.0) + public Alma(ITValuePublisher source, int period, double offset = 0.85, double sigma = 6.0) : this(period, offset, sigma) { source.Pub += (item) => Update(item); @@ -109,7 +95,7 @@ public sealed class Alma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -130,11 +116,11 @@ public sealed class Alma : ITValuePublisher } Last = new TValue(input.Time, result); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return new TSeries([], []); @@ -153,7 +139,7 @@ public sealed class Alma : ITValuePublisher // Restore state _buffer.Clear(); _state = default; - + // Replay last part to restore buffer state int startIndex = Math.Max(0, len - _period); for (int i = startIndex; i < len; i++) @@ -164,6 +150,14 @@ public sealed class Alma : ITValuePublisher return new TSeries(t, v); } + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double CalculateWeightedSum() { @@ -176,17 +170,17 @@ public sealed class Alma : ITValuePublisher // Buffer[0] (oldest) -> Weights[period - count] ReadOnlySpan bufferSpan = _buffer.GetSpan(); int weightOffset = _period - count; - + // Use DotProduct for partial sum double sum = bufferSpan.DotProduct(_weights.AsSpan(weightOffset, count)); - + // Calculate weightSum for this subset double wSum = 0; for (int i = 0; i < count; i++) { wSum += _weights[weightOffset + i]; } - + return wSum > 0 ? sum / wSum : 0; } @@ -194,20 +188,20 @@ public sealed class Alma : ITValuePublisher // We use InternalBuffer and StartIndex to avoid allocation and handle wrapping ReadOnlySpan internalBuf = _buffer.InternalBuffer; int head = _buffer.StartIndex; - + // Part 1: Oldest to End of Buffer -> InternalBuffer[Head ... Cap-1] // Matches Weights[0 ... Cap-Head-1] int part1Len = _period - head; double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len)); - + // Part 2: Start of Buffer to Newest -> InternalBuffer[0 ... Head-1] // Matches Weights[Cap-Head ... Cap-1] - double sum2 = internalBuf.Slice(0, head).DotProduct(_weights.AsSpan(part1Len)); - + double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len)); + return (sum1 + sum2) / _weightSum; } - public static TSeries Calculate(TSeries source, int period, double offset = 0.85, double sigma = 6.0) + public static TSeries Batch(TSeries source, int period, double offset = 0.85, double sigma = 6.0) { var alma = new Alma(period, offset, sigma); return alma.Update(source); @@ -260,39 +254,39 @@ public sealed class Alma : ITValuePublisher // Oldest is at: (bufferIdx - count + period) % period // But wait, the buffer wraps. // Let's just iterate 0..count-1 and map to buffer index. - + double sum = 0; double currentWeightSum = 0; - + int startIdx = (bufferIdx - count + period) % period; int weightOffset = period - count; // Align weights to end - + // Optimization: If full, we can use SIMD if we unwrap the buffer or handle wrapping. // For simplicity in static method (and since we can't easily unwrap stackalloc), // we'll use scalar loop with modulo. // Or better: copy to a temporary linear buffer? No, that's too much copying. - + // Actually, for full period, we can do two loops (part1, part2) to avoid modulo in loop. - + if (count == period) { // Buffer is full. startIdx is bufferIdx (which is the oldest, since we just wrote to bufferIdx-1) // Wait, bufferIdx points to the NEXT write position. // So bufferIdx is the Oldest. - + // Part 1: bufferIdx to End int part1Len = period - bufferIdx; for (int j = 0; j < part1Len; j++) { sum += buffer[bufferIdx + j] * weights[j]; } - + // Part 2: 0 to bufferIdx for (int j = 0; j < bufferIdx; j++) { sum += buffer[j] * weights[part1Len + j]; } - + output[i] = sum / weightSum; } else @@ -310,7 +304,7 @@ public sealed class Alma : ITValuePublisher } } - public void Reset() + public override void Reset() { _buffer.Clear(); _state = default; diff --git a/lib/trends/alma/Alma.md b/lib/trends/alma/Alma.md index 773037d4..5e3fc55b 100644 --- a/lib/trends/alma/Alma.md +++ b/lib/trends/alma/Alma.md @@ -57,7 +57,7 @@ double[] prices = ...; double[] output = new double[prices.Length]; // Calculate ALMA for the entire array -Alma.Calculate(prices.AsSpan(), output.AsSpan(), period: 9, offset: 0.85, sigma: 6.0); +Alma.Batch(prices.AsSpan(), output.AsSpan(), period: 9, offset: 0.85, sigma: 6.0); ``` ### Bar Correction diff --git a/lib/trends/conv/Conv.Quantower.Tests.cs b/lib/trends/conv/Conv.Quantower.Tests.cs index fd1720c5..d8c389ac 100644 --- a/lib/trends/conv/Conv.Quantower.Tests.cs +++ b/lib/trends/conv/Conv.Quantower.Tests.cs @@ -117,7 +117,7 @@ public class ConvIndicatorTests { var indicator = new ConvIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(ConvIndicator), method.DeclaringType); @@ -170,10 +170,10 @@ public class ConvIndicatorTests public void ConvIndicator_InvalidWeights_FallsBackToDefault() { var indicator = new ConvIndicator { WeightsInput = "invalid" }; - + // Should not throw, but fallback indicator.Initialize(); - + Assert.Single(indicator.LinesSeries); } diff --git a/lib/trends/conv/Conv.Quantower.cs b/lib/trends/conv/Conv.Quantower.cs index 46232b6a..4761320f 100644 --- a/lib/trends/conv/Conv.Quantower.cs +++ b/lib/trends/conv/Conv.Quantower.cs @@ -45,7 +45,7 @@ public class ConvIndicator : Indicator, IWatchlistIndicator var weights = WeightsInput.Split(',') .Select(s => double.Parse(s.Trim())) .ToArray(); - + if (weights.Length == 0) throw new ArgumentException("Weights cannot be empty"); diff --git a/lib/trends/conv/Conv.Tests.cs b/lib/trends/conv/Conv.Tests.cs index 02f5e69a..3be1312b 100644 --- a/lib/trends/conv/Conv.Tests.cs +++ b/lib/trends/conv/Conv.Tests.cs @@ -94,7 +94,7 @@ public class ConvTests source.Add(new TValue(DateTime.UtcNow, 3)); source.Add(new TValue(DateTime.UtcNow, 4)); - var result = Conv.Calculate(source, kernel); + var result = Conv.Batch(source, kernel); Assert.Equal(1.0, result.Values[0]); Assert.Equal(2.5, result.Values[1]); @@ -173,14 +173,14 @@ public class ConvTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Conv.Calculate(series, kernel); + var batchSeries = Conv.Batch(series, kernel); double expected = batchSeries.Last.Value; // 2. Span Mode var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Conv.Calculate(spanInput, spanOutput, kernel); + Conv.Batch(spanInput, spanOutput, kernel); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -214,8 +214,8 @@ public class ConvTests double[] wrongSizeOutput = new double[3]; double[] kernel = [0.5, 0.5]; - Assert.Throws(() => Conv.Calculate(source.AsSpan(), output.AsSpan(), Array.Empty())); - Assert.Throws(() => Conv.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), kernel)); + Assert.Throws(() => Conv.Batch(source.AsSpan(), output.AsSpan(), Array.Empty())); + Assert.Throws(() => Conv.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), kernel)); } [Fact] @@ -225,7 +225,7 @@ public class ConvTests double[] output = new double[5]; double[] kernel = [0.5, 0.5]; - Conv.Calculate(source.AsSpan(), output.AsSpan(), kernel); + Conv.Batch(source.AsSpan(), output.AsSpan(), kernel); foreach (var val in output) { diff --git a/lib/trends/conv/Conv.cs b/lib/trends/conv/Conv.cs index 5ce0a952..2a73e229 100644 --- a/lib/trends/conv/Conv.cs +++ b/lib/trends/conv/Conv.cs @@ -19,7 +19,7 @@ namespace QuanTAlib; /// Update: O(K) where K is kernel length. /// [SkipLocalsInit] -public sealed class Conv : ITValuePublisher +public sealed class Conv : AbstractBase { private readonly int _period; private readonly double[] _kernel; @@ -29,10 +29,7 @@ public sealed class Conv : ITValuePublisher private State _state; private State _p_state; - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _buffer.IsFull; - public event Action? Pub; + public override bool IsHot => _buffer.IsFull; public Conv(double[] kernel) { @@ -44,6 +41,7 @@ public sealed class Conv : ITValuePublisher Array.Copy(kernel, _kernel, _period); _buffer = new RingBuffer(_period); Name = $"Conv({_period})"; + WarmupPeriod = _period; _state.LastValidValue = double.NaN; _p_state.LastValidValue = double.NaN; } @@ -65,7 +63,7 @@ public sealed class Conv : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -92,29 +90,29 @@ public sealed class Conv : ITValuePublisher { int count = _buffer.Count; int kernelOffset = _period - count; - ReadOnlySpan kernelSpan = _kernel.AsSpan().Slice(kernelOffset); + ReadOnlySpan kernelSpan = _kernel.AsSpan()[kernelOffset..]; ReadOnlySpan internalBuf = _buffer.InternalBuffer; if (count < _period) { - result = internalBuf.Slice(0, count).DotProduct(kernelSpan); + result = internalBuf[..count].DotProduct(kernelSpan); } else { // Full: data is split at StartIndex (which points to oldest) int head = _buffer.StartIndex; int part1Len = _period - head; - result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan.Slice(0, part1Len)) - + internalBuf.Slice(0, head).DotProduct(kernelSpan.Slice(part1Len)); + result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan[..part1Len]) + + internalBuf[..head].DotProduct(kernelSpan[part1Len..]); } } Last = new TValue(input.Time, result); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -130,7 +128,7 @@ public sealed class Conv : ITValuePublisher source.Times.CopyTo(tSpan); var sourceValues = source.Values; - Calculate(sourceValues, vSpan, _kernel); + Batch(sourceValues, vSpan, _kernel); // Restore state // We need to replay the last few updates to restore _buffer and _lastValidValue @@ -172,14 +170,22 @@ public sealed class Conv : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, double[] kernel) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, double[] kernel) { var conv = new Conv(kernel); return conv.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, double[] kernel) + public static void Batch(ReadOnlySpan source, Span output, double[] kernel) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); @@ -223,21 +229,21 @@ public sealed class Conv : ITValuePublisher { int kernelOffset = period - count; // Window is [0..count-1] - sum = window.Slice(0, count).DotProduct(kernelSpan.Slice(kernelOffset)); + sum = window[..count].DotProduct(kernelSpan[kernelOffset..]); } else { // Full buffer - branchless version int part1Len = period - windowIdx; - sum = window.Slice(windowIdx, part1Len).DotProduct(kernelSpan.Slice(0, part1Len)) - + window.Slice(0, windowIdx).DotProduct(kernelSpan.Slice(part1Len)); + sum = window.Slice(windowIdx, part1Len).DotProduct(kernelSpan[..part1Len]) + + window[..windowIdx].DotProduct(kernelSpan[part1Len..]); } output[i] = sum; } } - public void Reset() + public override void Reset() { _buffer.Clear(); _state.LastValidValue = double.NaN; diff --git a/lib/trends/conv/Conv.md b/lib/trends/conv/Conv.md index d92e0f16..05901be6 100644 --- a/lib/trends/conv/Conv.md +++ b/lib/trends/conv/Conv.md @@ -55,7 +55,7 @@ double[] weights = { 0.1, 0.2, 0.3, 0.4 }; ReadOnlySpan input = ...; Span output = new double[input.Length]; -Conv.Calculate(input, output, weights); +Conv.Batch(input, output, weights); ``` ### Bar Correction diff --git a/lib/trends/dema/Dema.Quantower.Tests.cs b/lib/trends/dema/Dema.Quantower.Tests.cs index b48b7cdf..3816aeeb 100644 --- a/lib/trends/dema/Dema.Quantower.Tests.cs +++ b/lib/trends/dema/Dema.Quantower.Tests.cs @@ -116,7 +116,7 @@ public class DemaIndicatorTests { var indicator = new DemaIndicator(); indicator.Initialize(); - + // We can't easily mock PaintChartEventArgs fully, but we can verify the method exists and is callable // if we could mock the args. Since we can't, we skip the actual call but verify the method is overridden. var method = indicator.GetType().GetMethod("OnPaintChart"); diff --git a/lib/trends/dema/Dema.Tests.cs b/lib/trends/dema/Dema.Tests.cs index 4dd15251..8c749c75 100644 --- a/lib/trends/dema/Dema.Tests.cs +++ b/lib/trends/dema/Dema.Tests.cs @@ -46,7 +46,7 @@ public class DemaTests } // Act - var demaSeries = Dema.Calculate(source, period); + var demaSeries = Dema.Batch(source, period); var demaObj = new Dema(period); // Assert @@ -123,7 +123,7 @@ public class DemaTests } // Act - var demaSeries = Dema.Calculate(source, alpha); + var demaSeries = Dema.Batch(source, alpha); var demaObj = new Dema(alpha); // Assert @@ -271,7 +271,7 @@ public class DemaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Dema.Calculate(series, period); + var batchSeries = Dema.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/dema/Dema.cs b/lib/trends/dema/Dema.cs index 62c49c64..c0c6ff85 100644 --- a/lib/trends/dema/Dema.cs +++ b/lib/trends/dema/Dema.cs @@ -22,7 +22,7 @@ namespace QuanTAlib; /// Becomes true when the second EMA converges (approx. 2x EMA convergence time). /// [SkipLocalsInit] -public sealed class Dema : ITValuePublisher +public sealed class Dema : AbstractBase { private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated) { @@ -31,19 +31,16 @@ public sealed class Dema : ITValuePublisher private readonly double _alpha; private readonly double _decay; - + private EmaState _state1 = EmaState.New(); private EmaState _state2 = EmaState.New(); private EmaState _p_state1 = EmaState.New(); private EmaState _p_state2 = EmaState.New(); - + private double _lastValidValue; private double _p_lastValidValue; - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _state2.IsHot; - public event Action? Pub; + public override bool IsHot => _state2.IsHot; public Dema(int period) { @@ -52,6 +49,7 @@ public sealed class Dema : ITValuePublisher _alpha = 2.0 / (period + 1); _decay = 1.0 - _alpha; Name = $"Dema({period})"; + WarmupPeriod = period; } public Dema(ITValuePublisher source, int period) : this(period) @@ -69,7 +67,7 @@ public sealed class Dema : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -98,11 +96,11 @@ public sealed class Dema : ITValuePublisher double result = 2 * e1 - e2; Last = new TValue(input.Time, result); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -117,7 +115,7 @@ public sealed class Dema : ITValuePublisher source.Times.CopyTo(tSpan); var sourceValues = source.Values; - + // Use current state EmaState s1 = _state1; EmaState s2 = _state2; @@ -151,6 +149,14 @@ public sealed class Dema : ITValuePublisher return new TSeries(t, v); } + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double Compute(double input, double alpha, double decay, ref EmaState state) { @@ -182,13 +188,13 @@ public sealed class Dema : ITValuePublisher return result; } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var dema = new Dema(period); return dema.Update(source); } - public static TSeries Calculate(TSeries source, double alpha) + public static TSeries Batch(TSeries source, double alpha) { var dema = new Dema(alpha); return dema.Update(source); @@ -280,7 +286,7 @@ public sealed class Dema : ITValuePublisher } } - public void Reset() + public override void Reset() { _state1 = EmaState.New(); _state2 = EmaState.New(); diff --git a/lib/trends/dema/Dema.md b/lib/trends/dema/Dema.md index 223379b9..2eb2b661 100644 --- a/lib/trends/dema/Dema.md +++ b/lib/trends/dema/Dema.md @@ -62,12 +62,12 @@ Console.WriteLine($"Current DEMA: {result.Value}"); // Batch calculation (TSeries API) TSeries source = ...; -TSeries results = Dema.Calculate(source, 14); +TSeries results = Dema.Batch(source, 14); // High-performance Span API (zero allocation) double[] prices = new double[10000]; double[] output = new double[10000]; -Dema.Calculate(prices.AsSpan(), output.AsSpan(), period: 14); +Dema.Batch(prices.AsSpan(), output.AsSpan(), period: 14); ``` ### Zero-Allocation Span API @@ -80,7 +80,7 @@ double[] source = new double[200000]; double[] demaOutput = new double[200000]; // Zero heap allocation during calculation -Dema.Calculate(source.AsSpan(), demaOutput.AsSpan(), period: 50); +Dema.Batch(source.AsSpan(), demaOutput.AsSpan(), period: 50); ``` ### Eventing and Reactive Support diff --git a/lib/trends/dwma/Dwma.Tests.cs b/lib/trends/dwma/Dwma.Tests.cs index 9e19054d..f80d4281 100644 --- a/lib/trends/dwma/Dwma.Tests.cs +++ b/lib/trends/dwma/Dwma.Tests.cs @@ -94,7 +94,7 @@ public class DwmaTests dwma.Update(source.Last); } - var staticResult = Dwma.Calculate(source, period); + var staticResult = Dwma.Batch(source, period); Assert.Equal(source.Count, staticResult.Count); Assert.Equal(dwma.Last.Value, staticResult.Last.Value, 8); @@ -180,7 +180,7 @@ public class DwmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Dwma.Calculate(series, period); + var batchSeries = Dwma.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/dwma/Dwma.cs b/lib/trends/dwma/Dwma.cs index 6e500a5d..fb46ca7f 100644 --- a/lib/trends/dwma/Dwma.cs +++ b/lib/trends/dwma/Dwma.cs @@ -15,28 +15,13 @@ namespace QuanTAlib; /// DWMA = WMA(WMA(source, period), period) /// [SkipLocalsInit] -public sealed class Dwma : ITValuePublisher +public sealed class Dwma : AbstractBase { private readonly int _period; private readonly Wma _wma1; private readonly Wma _wma2; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - /// - /// Current DWMA value. - /// - public TValue Last { get; private set; } - - /// - /// True if the indicator has enough data to produce valid results. - /// - public bool IsHot => _wma1.IsHot && _wma2.IsHot; - - public event Action? Pub; + public override bool IsHot => _wma1.IsHot && _wma2.IsHot; /// /// Creates DWMA with specified period. @@ -51,6 +36,7 @@ public sealed class Dwma : ITValuePublisher _wma1 = new Wma(period); _wma2 = new Wma(period); Name = $"Dwma({period})"; + WarmupPeriod = period * 2; } public Dwma(ITValuePublisher source, int period) : this(period) @@ -59,15 +45,15 @@ public sealed class Dwma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { TValue wma1Result = _wma1.Update(input, isNew); Last = _wma2.Update(wma1Result, isNew); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -87,13 +73,13 @@ public sealed class Dwma : ITValuePublisher // We need to replay the last part to restore the internal WMAs state // Since DWMA is WMA(WMA), the effective lookback is roughly 2*Period // But to be safe and simple, we can just reset and replay the last 2*Period bars. - + _wma1.Reset(); _wma2.Reset(); - + int warmup = _period * 2; // Approximate warmup needed int startIndex = Math.Max(0, len - warmup); - + for (int i = startIndex; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i])); @@ -102,7 +88,16 @@ public sealed class Dwma : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public override void Prime(ReadOnlySpan source) + { + Reset(); + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period) { var dwma = new Dwma(period); return dwma.Update(source); @@ -119,18 +114,18 @@ public sealed class Dwma : ITValuePublisher if (source.Length <= 1024) { Span temp = stackalloc double[source.Length]; - Wma.Calculate(source, temp, period); - Wma.Calculate(temp, output, period); + Wma.Batch(source, temp, period); + Wma.Batch(temp, output, period); } else { double[] temp = new double[source.Length]; - Wma.Calculate(source, temp, period); - Wma.Calculate(temp, output, period); + Wma.Batch(source, temp, period); + Wma.Batch(temp, output, period); } } - - public void Reset() + + public override void Reset() { _wma1.Reset(); _wma2.Reset(); diff --git a/lib/trends/dwma/Dwma.md b/lib/trends/dwma/Dwma.md index bad660d4..6cd9596e 100644 --- a/lib/trends/dwma/Dwma.md +++ b/lib/trends/dwma/Dwma.md @@ -91,7 +91,7 @@ Console.WriteLine($"DWMA: {result.Value}"); ReadOnlySpan input = ...; Span output = new double[input.Length]; -Dwma.Calculate(input, output, 14); +Dwma.Batch(input, output, 14); ``` ### Bar Correction diff --git a/lib/trends/ema/Ema.Quantower.cs b/lib/trends/ema/Ema.Quantower.cs index 34565fff..e0c7ccb3 100644 --- a/lib/trends/ema/Ema.Quantower.cs +++ b/lib/trends/ema/Ema.Quantower.cs @@ -1,65 +1,65 @@ -using System.Drawing; -using TradingPlatform.BusinessLayer; - -namespace QuanTAlib; - -public class EmaIndicator : Indicator, IWatchlistIndicator -{ - [InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)] - public int Period { get; set; } = 10; - - [IndicatorExtensions.DataSourceInput] - public SourceType Source { get; set; } = SourceType.Close; - - [InputParameter("Show cold values", sortIndex: 21)] - public bool ShowColdValues { get; set; } = true; - - private Ema? ma; - protected LineSeries? Series; - protected string? SourceName; - private int _warmupBarIndex = -1; - - public int MinHistoryDepths => Period; - int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; - - public override string ShortName => $"EMA {Period}:{SourceName}"; - - public EmaIndicator() - { - OnBackGround = true; - SeparateWindow = false; - SourceName = Source.ToString(); - Name = "EMA - Exponential Moving Average"; - Description = "Exponential Moving Average"; - Series = new(name: $"EMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid); - AddLineSeries(Series); - } - - protected override void OnInit() - { - ma = new Ema(Period); - SourceName = Source.ToString(); - _warmupBarIndex = -1; // Reset warmup tracking when period changes - base.OnInit(); - } - - protected override void OnUpdate(UpdateArgs args) - { - TValue input = this.GetInputValue(args, Source); - bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; - TValue result = ma!.Update(input, isNew); - Series!.SetValue(result.Value); - Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here - - // Track when IsHot becomes true for the first time - if (_warmupBarIndex < 0 && ma!.IsHot) - _warmupBarIndex = Count; - } - - public override void OnPaintChart(PaintChartEventArgs args) - { - base.OnPaintChart(args); - int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count; - this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2); - } -} +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class EmaIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)] + public int Period { get; set; } = 10; + + [IndicatorExtensions.DataSourceInput] + public SourceType Source { get; set; } = SourceType.Close; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Ema? ma; + protected LineSeries? Series; + protected string? SourceName; + private int _warmupBarIndex = -1; + + public int MinHistoryDepths => Period; + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public override string ShortName => $"EMA {Period}:{SourceName}"; + + public EmaIndicator() + { + OnBackGround = true; + SeparateWindow = false; + SourceName = Source.ToString(); + Name = "EMA - Exponential Moving Average"; + Description = "Exponential Moving Average"; + Series = new(name: $"EMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid); + AddLineSeries(Series); + } + + protected override void OnInit() + { + ma = new Ema(Period); + SourceName = Source.ToString(); + _warmupBarIndex = -1; // Reset warmup tracking when period changes + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TValue input = this.GetInputValue(args, Source); + bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; + TValue result = ma!.Update(input, isNew); + Series!.SetValue(result.Value); + Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here + + // Track when IsHot becomes true for the first time + if (_warmupBarIndex < 0 && ma!.IsHot) + _warmupBarIndex = Count; + } + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count; + this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2); + } +} diff --git a/lib/trends/ema/Ema.Tests.cs b/lib/trends/ema/Ema.Tests.cs index ffc6e40b..4679a73c 100644 --- a/lib/trends/ema/Ema.Tests.cs +++ b/lib/trends/ema/Ema.Tests.cs @@ -363,34 +363,34 @@ public class EmaTests // ============== Span API Tests ============== [Fact] - public void Ema_SpanCalc_Period_ValidatesInput() + public void Ema_SpanBatch_Period_ValidatesInput() { double[] source = [1, 2, 3, 4, 5]; double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; // Period must be > 0 - Assert.Throws(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), -1)); + Assert.Throws(() => Ema.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Ema.Batch(source.AsSpan(), output.AsSpan(), -1)); // Output must be same length as source - Assert.Throws(() => Ema.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Ema.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] - public void Ema_SpanCalc_Alpha_ValidatesInput() + public void Ema_SpanBatch_Alpha_ValidatesInput() { double[] source = [1, 2, 3, 4, 5]; double[] output = new double[5]; // Alpha must be > 0 and <= 1 - Assert.Throws(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), 0.0)); - Assert.Throws(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), -0.1)); - Assert.Throws(() => Ema.Calculate(source.AsSpan(), output.AsSpan(), 1.1)); + Assert.Throws(() => Ema.Batch(source.AsSpan(), output.AsSpan(), 0.0)); + Assert.Throws(() => Ema.Batch(source.AsSpan(), output.AsSpan(), -0.1)); + Assert.Throws(() => Ema.Batch(source.AsSpan(), output.AsSpan(), 1.1)); } [Fact] - public void Ema_SpanCalc_MatchesTSeriesCalc() + public void Ema_SpanBatch_MatchesTSeriesBatch() { var series = new TSeries(); double[] source = new double[100]; @@ -405,10 +405,10 @@ public class EmaTests } // Calculate with TSeries API - var tseriesResult = Ema.Calculate(series, 10); + var tseriesResult = Ema.Batch(series, 10); // Calculate with Span API - Ema.Calculate(source.AsSpan(), output.AsSpan(), 10); + Ema.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results - allow small tolerance due to bias correction differences for (int i = 0; i < 100; i++) @@ -418,7 +418,7 @@ public class EmaTests } [Fact] - public void Ema_SpanCalc_PeriodAndAlphaEquivalent() + public void Ema_SpanBatch_PeriodAndAlphaEquivalent() { double[] source = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; double[] outputPeriod = new double[10]; @@ -427,8 +427,8 @@ public class EmaTests int period = 5; double alpha = 2.0 / (period + 1); - Ema.Calculate(source.AsSpan(), outputPeriod.AsSpan(), period); - Ema.Calculate(source.AsSpan(), outputAlpha.AsSpan(), alpha); + Ema.Batch(source.AsSpan(), outputPeriod.AsSpan(), period); + Ema.Batch(source.AsSpan(), outputAlpha.AsSpan(), alpha); // Results should be identical for (int i = 0; i < 10; i++) @@ -438,7 +438,7 @@ public class EmaTests } [Fact] - public void Ema_SpanCalc_ZeroAllocation() + public void Ema_SpanBatch_ZeroAllocation() { double[] source = new double[10000]; double[] output = new double[10000]; @@ -448,19 +448,19 @@ public class EmaTests source[i] = gbm.Next().Close; // Warm up - Ema.Calculate(source.AsSpan(), output.AsSpan(), 100); + Ema.Batch(source.AsSpan(), output.AsSpan(), 100); // This test verifies the method runs without throwing Assert.True(double.IsFinite(output[^1])); } [Fact] - public void Ema_SpanCalc_HandlesNaN() + public void Ema_SpanBatch_HandlesNaN() { double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Ema.Calculate(source.AsSpan(), output.AsSpan(), 3); + Ema.Batch(source.AsSpan(), output.AsSpan(), 3); // All outputs should be finite foreach (var val in output) @@ -470,12 +470,12 @@ public class EmaTests } [Fact] - public void Ema_SpanCalc_BiasCorrection_Works() + public void Ema_SpanBatch_BiasCorrection_Works() { double[] source = [100, 100, 100, 100, 100]; double[] output = new double[5]; - Ema.Calculate(source.AsSpan(), output.AsSpan(), 3); + Ema.Batch(source.AsSpan(), output.AsSpan(), 3); // With bias correction, first value should equal input Assert.Equal(100.0, output[0], 1e-10); @@ -488,18 +488,97 @@ public class EmaTests } [Fact] - public void Ema_SpanCalc_Alpha_DirectUsage() + public void Ema_SpanBatch_Alpha_DirectUsage() { double[] source = [10, 20, 30, 40, 50]; double[] output = new double[5]; // Use alpha = 0.5 directly - Ema.Calculate(source.AsSpan(), output.AsSpan(), 0.5); + Ema.Batch(source.AsSpan(), output.AsSpan(), 0.5); // Results should be finite and reasonable Assert.True(double.IsFinite(output[^1])); Assert.True(output[^1] > 10 && output[^1] <= 50); } + + [Fact] + public void Chainability_Works() + { + var source = new TSeries(); + var ema = new Ema(source, 10); + + source.Add(new TValue(DateTime.UtcNow, 100)); + Assert.Equal(100, ema.Last.Value, 1e-10); + } + + [Fact] + public void Prime_SetsStateCorrectly() + { + var ema = new Ema(5); + double[] history = [10, 20, 30, 40, 50]; + + ema.Prime(history); + + // EMA(5) of 10,20,30,40,50 + // Alpha = 2/6 = 1/3 + // 10 -> 10 + // 20 -> 10 + 1/3(10) = 13.33... + // ... + // We can verify against a fresh EMA fed with same data + var verifyEma = new Ema(5); + foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val)); + + Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10); + Assert.Equal(verifyEma.IsHot, ema.IsHot); + + // Verify it continues correctly + ema.Update(new TValue(DateTime.UtcNow, 60)); + verifyEma.Update(new TValue(DateTime.UtcNow, 60)); + Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10); + } + + [Fact] + public void Prime_HandlesNaN_InHistory() + { + var ema = new Ema(5); + double[] history = [10, 20, double.NaN, 40, 50]; + + ema.Prime(history); + + var verifyEma = new Ema(5); + foreach (var val in history) verifyEma.Update(new TValue(DateTime.UtcNow, val)); + + Assert.Equal(verifyEma.Last.Value, ema.Last.Value, 1e-10); + } + + [Fact] + public void Calculate_ReturnsCorrectResultsAndHotIndicator() + { + var series = new TSeries(); + for (int i = 1; i <= 20; i++) series.Add(DateTime.UtcNow, i * 10); + + // EMA(5) + var (results, indicator) = Ema.Calculate(series, 5); + + // Check results + Assert.Equal(20, results.Count); + + // Verify against standard calculation + var verifyEma = new Ema(5); + var verifyResults = verifyEma.Update(series); + + Assert.Equal(verifyResults.Last.Value, results.Last.Value, 1e-10); + Assert.Equal(verifyEma.Last.Value, indicator.Last.Value, 1e-10); + + // Check indicator state + Assert.True(indicator.IsHot); + + // Verify indicator continues correctly + indicator.Update(new TValue(DateTime.UtcNow, 210)); + verifyEma.Update(new TValue(DateTime.UtcNow, 210)); + Assert.Equal(verifyEma.Last.Value, indicator.Last.Value, 1e-10); + } + [Fact] public void Ema_AllModes_ProduceSameResult() { @@ -510,14 +589,14 @@ public class EmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Ema.Calculate(series, period); + var batchSeries = Ema.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode var tValues = series.Values.ToArray(); // Need array for Span modification safety if any var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Ema.Calculate(spanInput, spanOutput, period); + Ema.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode diff --git a/lib/trends/ema/Ema.Validation.Tests.cs b/lib/trends/ema/Ema.Validation.Tests.cs index 639a40aa..6017988b 100644 --- a/lib/trends/ema/Ema.Validation.Tests.cs +++ b/lib/trends/ema/Ema.Validation.Tests.cs @@ -91,7 +91,7 @@ public class EmaValidationTests : IDisposable { // Calculate QuanTAlib EMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Ema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate Skender EMA var sResult = _testData.SkenderQuotes.GetEma(period).ToList(); @@ -173,7 +173,7 @@ public class EmaValidationTests : IDisposable { // Calculate QuanTAlib EMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Ema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate TA-Lib EMA var retCode = TALib.Functions.Ema(sourceData, 0..^0, talibOutput, out var outRange, period); @@ -261,7 +261,7 @@ public class EmaValidationTests : IDisposable { // Calculate QuanTAlib EMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Ema.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Ema.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate Tulip EMA var emaIndicator = Tulip.Indicators.ema; diff --git a/lib/trends/ema/Ema.cs b/lib/trends/ema/Ema.cs index a72b88f6..9e318bb3 100644 --- a/lib/trends/ema/Ema.cs +++ b/lib/trends/ema/Ema.cs @@ -1,296 +1,394 @@ -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace QuanTAlib; - -/// -/// EMA: Exponential Moving Average -/// -/// -/// EMA applies exponential weighting to data points, giving more weight to recent values. -/// Uses a single state variable for O(1) complexity per update. -/// -/// Calculation: -/// alpha = 2 / (period + 1) -/// EMA_new = EMA_old + alpha * (newest - EMA_old) -/// -/// Initialization: -/// Uses a compensator factor to correct early-stage bias (when n < period). -/// Output = EMA_state / (1 - (1-alpha)^n) -/// -/// O(1) update: -/// No buffer required, only previous EMA value and compensator state. -/// -/// IsHot: -/// Becomes true when n = ln(0.05) / ln(1 - alpha) -/// -[SkipLocalsInit] -public sealed class Ema : ITValuePublisher -{ - private record struct State(double Ema, double E, bool IsHot, bool IsCompensated) - { - public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false }; - } - - private readonly double _alpha; - private readonly double _decay; - private State _state = State.New(); - private State _p_state = State.New(); - private double _lastValidValue; - private double _p_lastValidValue; - - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; - - /// - /// Creates EMA with specified period. - /// Alpha = 2 / (period + 1) - /// - /// Period for EMA calculation (must be > 0) - public Ema(int period) - { - if (period <= 0) - throw new ArgumentException("Period must be greater than 0", nameof(period)); - - _alpha = 2.0 / (period + 1); - _decay = 1.0 - _alpha; - Name = $"Ema({period})"; - } - - /// - /// Creates EMA with specified source and period. - /// Subscribes to source.Pub event. - /// - /// Source to subscribe to - /// Period for EMA calculation - public Ema(ITValuePublisher source, int period) : this(period) - { - source.Pub += (item) => Update(item); - } - - /// - /// Creates EMA with specified alpha smoothing factor. - /// - /// Smoothing factor (0 < alpha <= 1) - public Ema(double alpha) - { - if (alpha <= 0 || alpha > 1) - throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); - - _alpha = alpha; - _decay = 1.0 - alpha; - Name = $"Ema(α={alpha:F4})"; - } - - /// - /// Current EMA value. - /// - public TValue Last { get; private set; } - - /// - /// True if the EMA has warmed up and is providing valid results. - /// - public bool IsHot => _state.IsHot; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private double GetValidValue(double input) - { - if (double.IsFinite(input)) - { - _lastValidValue = input; - return input; - } - return _lastValidValue; - } - - private const double COVERAGE_THRESHOLD = 0.05; - private const double COMPENSATOR_THRESHOLD = 1e-10; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) - { - if (isNew) - { - _p_state = _state; - _p_lastValidValue = _lastValidValue; - } - else - { - _state = _p_state; - _lastValidValue = _p_lastValidValue; - } - - double val = GetValidValue(input.Value); - val = Compute(val, _alpha, _decay, ref _state); - Last = new TValue(input.Time, val); - Pub?.Invoke(Last); - return Last; - } - - public TSeries Update(TSeries source) - { - if (source.Count == 0) return []; - - int len = source.Count; - var t = new List(len); - var v = new List(len); - CollectionsMarshal.SetCount(t, len); - CollectionsMarshal.SetCount(v, len); - - var tSpan = CollectionsMarshal.AsSpan(t); - var vSpan = CollectionsMarshal.AsSpan(v); - var sourceValues = source.Values; - var sourceTimes = source.Times; - - State state = _state; - double lastValidValue = _lastValidValue; - - CalculateCore(sourceValues, vSpan, _alpha, ref state, ref lastValidValue); - - _state = state; - _lastValidValue = lastValidValue; - - sourceTimes.CopyTo(tSpan); - - _p_state = _state; - _p_lastValidValue = _lastValidValue; - Last = new TValue(tSpan[len - 1], vSpan[len - 1]); - - return new TSeries(t, v); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static double Compute(double input, double alpha, double decay, ref State state) - { - state.Ema += alpha * (input - state.Ema); - - double result; - if (!state.IsCompensated) - { - state.E *= decay; - - if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) - state.IsHot = true; - - if (state.E <= COMPENSATOR_THRESHOLD) - { - state.IsCompensated = true; - result = state.Ema; - } - else - { - result = state.Ema / (1.0 - state.E); - } - } - else - { - result = state.Ema; - } - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CalculateCore(ReadOnlySpan source, Span output, double alpha, ref State state, ref double lastValidValue) - { - int len = source.Length; - double decay = 1.0 - alpha; - int i = 0; - - if (!state.IsCompensated) - { - for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++) - { - double val = source[i]; - if (double.IsFinite(val)) - lastValidValue = val; - else - val = lastValidValue; - - state.Ema += alpha * (val - state.Ema); - state.E *= decay; - - if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) - state.IsHot = true; - - output[i] = state.Ema / (1.0 - state.E); - } - if (state.E <= COMPENSATOR_THRESHOLD) - state.IsCompensated = true; - } - - for (; i < len; i++) - { - double val = source[i]; - if (double.IsFinite(val)) - lastValidValue = val; - else - val = lastValidValue; - - state.Ema += alpha * (val - state.Ema); - output[i] = state.Ema; - } - } - - /// - /// Calculates EMA for the entire series using a new instance. - /// - /// Input series - /// EMA period - /// EMA series - public static TSeries Calculate(TSeries source, int period) - { - var ema = new Ema(period); - return ema.Update(source); - } - - /// - /// Calculates EMA in-place using period, writing results to pre-allocated output span. - /// Zero-allocation method for maximum performance. - /// Alpha = 2 / (period + 1) - /// - /// Input values - /// Output span (must be same length as source) - /// EMA period (must be > 0) - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) - { - if (period <= 0) - throw new ArgumentException("Period must be greater than 0", nameof(period)); - - double alpha = 2.0 / (period + 1); - Calculate(source, output, alpha); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, double alpha) - { - if (source.Length != output.Length) - throw new ArgumentException("Source and output must have the same length"); - if (alpha <= 0 || alpha > 1) - throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); - - if (source.Length == 0) return; - - var state = State.New(); - double lastValid = 0; - - CalculateCore(source, output, alpha, ref state, ref lastValid); - } - - /// - /// Resets the EMA state. - /// - public void Reset() - { - _state = State.New(); - _p_state = _state; - _lastValidValue = 0; - _p_lastValidValue = 0; - Last = default; - } -} +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace QuanTAlib; + +/// +/// EMA: Exponential Moving Average +/// +/// +/// EMA applies exponential weighting to data points, giving more weight to recent values. +/// Uses a single state variable for O(1) complexity per update. +/// +/// Calculation: +/// alpha = 2 / (period + 1) +/// EMA_new = EMA_old + alpha * (newest - EMA_old) +/// +/// Initialization: +/// Uses a compensator factor to correct early-stage bias (when n < period). +/// Output = EMA_state / (1 - (1-alpha)^n) +/// +/// O(1) update: +/// No buffer required, only previous EMA value and compensator state. +/// +/// IsHot: +/// Becomes true when n = ln(0.05) / ln(1 - alpha) +/// +[SkipLocalsInit] +public sealed class Ema : AbstractBase +{ + private record struct State(double Ema, double E, bool IsHot, bool IsCompensated) + { + public static State New() => new() { Ema = 0, E = 1.0, IsHot = false, IsCompensated = false }; + } + + private readonly double _alpha; + private readonly double _decay; + private State _state = State.New(); + private State _p_state = State.New(); + private double _lastValidValue; + private double _p_lastValidValue; + + /// + /// Creates EMA with specified period. + /// Alpha = 2 / (period + 1) + /// + /// Period for EMA calculation (must be > 0) + public Ema(int period) + { + if (period <= 0) + throw new ArgumentException("Period must be greater than 0", nameof(period)); + + _alpha = 2.0 / (period + 1); + _decay = 1.0 - _alpha; + Name = $"Ema({period})"; + WarmupPeriod = period; + } + + /// + /// Creates EMA with specified source and period. + /// Subscribes to source.Pub event. + /// + /// Source to subscribe to + /// Period for EMA calculation + public Ema(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + + public Ema(TSeries source, int period) : this(period) + { + Prime(source.Values); + if (source.Count > 0) + { + Last = new TValue(source.LastTime, Last.Value); + } + source.Pub += (item) => Update(item); + } + + /// + /// Creates EMA with specified alpha smoothing factor. + /// + /// Smoothing factor (0 < alpha <= 1) + public Ema(double alpha) + { + if (alpha <= 0 || alpha > 1) + throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); + + _alpha = alpha; + _decay = 1.0 - alpha; + Name = $"Ema(α={alpha:F4})"; + // Approximate period from alpha: alpha = 2/(N+1) => N = 2/alpha - 1 + WarmupPeriod = (int)(2.0 / alpha - 1.0); + } + + /// + /// True if the EMA has warmed up and is providing valid results. + /// + public override bool IsHot => _state.IsHot; + + /// + /// Initializes the indicator state using the provided history. + /// + /// Historical data + public override void Prime(ReadOnlySpan source) + { + if (source.Length == 0) return; + + // Reset state + _state = State.New(); + _p_state = State.New(); + _lastValidValue = 0; + _p_lastValidValue = 0; + + // Run the calculation on the history to update state + // We don't need the output, just the final state + int len = source.Length; + double decay = _decay; + int i = 0; + + // Find first valid value to seed lastValid + for (int k = 0; k < len; k++) + { + if (double.IsFinite(source[k])) + { + _lastValidValue = source[k]; + break; + } + } + + if (!_state.IsCompensated) + { + for (; i < len && _state.E > COMPENSATOR_THRESHOLD; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + _lastValidValue = val; + else + val = _lastValidValue; + + _state.Ema += _alpha * (val - _state.Ema); + _state.E *= decay; + + if (!_state.IsHot && _state.E <= COVERAGE_THRESHOLD) + _state.IsHot = true; + } + if (_state.E <= COMPENSATOR_THRESHOLD) + _state.IsCompensated = true; + } + + for (; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + _lastValidValue = val; + else + val = _lastValidValue; + + _state.Ema += _alpha * (val - _state.Ema); + } + + // Calculate the initial "Last" value + double result = _state.IsCompensated ? _state.Ema : _state.Ema / (1.0 - _state.E); + + // Note: We can't infer accurate Time from a simple Span, + // so we leave 'Last' with default time or user updates it on next Tick. + Last = new TValue(DateTime.MinValue, result); + + // Backup state for the next update cycle + _p_state = _state; + _p_lastValidValue = _lastValidValue; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private double GetValidValue(double input) + { + if (double.IsFinite(input)) + { + _lastValidValue = input; + return input; + } + return _lastValidValue; + } + + private const double COVERAGE_THRESHOLD = 0.05; + private const double COMPENSATOR_THRESHOLD = 1e-10; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override TValue Update(TValue input, bool isNew = true) + { + if (isNew) + { + _p_state = _state; + _p_lastValidValue = _lastValidValue; + } + else + { + _state = _p_state; + _lastValidValue = _p_lastValidValue; + } + + double val = GetValidValue(input.Value); + val = Compute(val, _alpha, _decay, ref _state); + Last = new TValue(input.Time, val); + PubEvent(Last); + return Last; + } + + public override TSeries Update(TSeries source) + { + if (source.Count == 0) return []; + + int len = source.Count; + var t = new List(len); + var v = new List(len); + CollectionsMarshal.SetCount(t, len); + CollectionsMarshal.SetCount(v, len); + + var tSpan = CollectionsMarshal.AsSpan(t); + var vSpan = CollectionsMarshal.AsSpan(v); + var sourceValues = source.Values; + var sourceTimes = source.Times; + + State state = _state; + double lastValidValue = _lastValidValue; + + CalculateCore(sourceValues, vSpan, _alpha, ref state, ref lastValidValue); + + _state = state; + _lastValidValue = lastValidValue; + + sourceTimes.CopyTo(tSpan); + + _p_state = _state; + _p_lastValidValue = _lastValidValue; + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); + + return new TSeries(t, v); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static double Compute(double input, double alpha, double decay, ref State state) + { + state.Ema += alpha * (input - state.Ema); + + double result; + if (!state.IsCompensated) + { + state.E *= decay; + + if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + state.IsHot = true; + + if (state.E <= COMPENSATOR_THRESHOLD) + { + state.IsCompensated = true; + result = state.Ema; + } + else + { + result = state.Ema / (1.0 - state.E); + } + } + else + { + result = state.Ema; + } + + return result; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CalculateCore(ReadOnlySpan source, Span output, double alpha, ref State state, ref double lastValidValue) + { + int len = source.Length; + double decay = 1.0 - alpha; + int i = 0; + + if (!state.IsCompensated) + { + for (; i < len && state.E > COMPENSATOR_THRESHOLD; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + lastValidValue = val; + else + val = lastValidValue; + + state.Ema += alpha * (val - state.Ema); + state.E *= decay; + + if (!state.IsHot && state.E <= COVERAGE_THRESHOLD) + state.IsHot = true; + + output[i] = state.Ema / (1.0 - state.E); + } + if (state.E <= COMPENSATOR_THRESHOLD) + state.IsCompensated = true; + } + + for (; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + lastValidValue = val; + else + val = lastValidValue; + + state.Ema += alpha * (val - state.Ema); + output[i] = state.Ema; + } + } + + /// + /// Runs a high-performance batch calculation on history and returns + /// a "Hot" Ema instance ready to process the next tick immediately. + /// + /// Historical time series + /// EMA Period + /// A tuple containing the full calculation results and the hot indicator instance + public static (TSeries Results, Ema Indicator) Calculate(TSeries source, int period) + { + var ema = new Ema(period); + TSeries results = ema.Update(source); + return (results, ema); + } + + /// + /// Calculates EMA for the entire series using a new instance. + /// + /// Input series + /// EMA period + /// EMA series + public static TSeries Batch(TSeries source, int period) + { + var ema = new Ema(period); + return ema.Update(source); + } + + /// + /// Calculates EMA in-place using period, writing results to pre-allocated output span. + /// Zero-allocation method for maximum performance. + /// Alpha = 2 / (period + 1) + /// + /// Input values + /// Output span (must be same length as source) + /// EMA period (must be > 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Batch(ReadOnlySpan source, Span output, int period) + { + if (period <= 0) + throw new ArgumentException("Period must be greater than 0", nameof(period)); + + double alpha = 2.0 / (period + 1); + Batch(source, output, alpha); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Batch(ReadOnlySpan source, Span output, double alpha) + { + if (source.Length != output.Length) + throw new ArgumentException("Source and output must have the same length"); + if (alpha <= 0 || alpha > 1) + throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha)); + + if (source.Length == 0) return; + + var state = State.New(); + double lastValid = 0; + + // Find first valid value to seed lastValid + for (int k = 0; k < source.Length; k++) + { + if (double.IsFinite(source[k])) + { + lastValid = source[k]; + break; + } + } + + CalculateCore(source, output, alpha, ref state, ref lastValid); + } + + /// + /// Resets the EMA state. + /// + public override void Reset() + { + _state = State.New(); + _p_state = _state; + _lastValidValue = 0; + _p_lastValidValue = 0; + Last = default; + } +} diff --git a/lib/trends/ema/Ema.md b/lib/trends/ema/Ema.md index c8fbaac8..48176b69 100644 --- a/lib/trends/ema/Ema.md +++ b/lib/trends/ema/Ema.md @@ -79,14 +79,14 @@ Console.WriteLine($"Current Value: {ema.Value.Value}"); // Batch calculation (TSeries API) TSeries source = ...; -TSeries results = Ema.Calculate(source, 10); +TSeries results = Ema.Batch(source, 10); // High-performance Span API (zero allocation) double[] prices = new double[10000]; double[] output = new double[10000]; -Ema.Calculate(prices.AsSpan(), output.AsSpan(), period: 10); +Ema.Batch(prices.AsSpan(), output.AsSpan(), period: 10); // Or with direct alpha: -Ema.Calculate(prices.AsSpan(), output.AsSpan(), alpha: 0.1818); +Ema.Batch(prices.AsSpan(), output.AsSpan(), alpha: 0.1818); ``` ### Zero-Allocation Span API @@ -99,10 +99,10 @@ double[] source = new double[200000]; double[] emaOutput = new double[200000]; // Zero heap allocation during calculation - by period -Ema.Calculate(source.AsSpan(), emaOutput.AsSpan(), period: 100); +Ema.Batch(source.AsSpan(), emaOutput.AsSpan(), period: 100); // Or by alpha for direct control -Ema.Calculate(source.AsSpan(), emaOutput.AsSpan(), alpha: 0.02); +Ema.Batch(source.AsSpan(), emaOutput.AsSpan(), alpha: 0.02); // Results are written directly to output buffer Console.WriteLine($"Last EMA: {emaOutput[^1]}"); diff --git a/lib/trends/hma/Hma.Quantower.Tests.cs b/lib/trends/hma/Hma.Quantower.Tests.cs index 20e40e95..e707aed4 100644 --- a/lib/trends/hma/Hma.Quantower.Tests.cs +++ b/lib/trends/hma/Hma.Quantower.Tests.cs @@ -117,7 +117,7 @@ public class HmaIndicatorTests { var indicator = new HmaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(HmaIndicator), method.DeclaringType); diff --git a/lib/trends/hma/Hma.Tests.cs b/lib/trends/hma/Hma.Tests.cs index 897dc996..678ed288 100644 --- a/lib/trends/hma/Hma.Tests.cs +++ b/lib/trends/hma/Hma.Tests.cs @@ -90,7 +90,7 @@ public class HmaTests } var instanceResults = new Hma(14).Update(series); - var staticResults = Hma.Calculate(series, 14); + var staticResults = Hma.Batch(series, 14); for (int i = 0; i < instanceResults.Count; i++) { @@ -109,7 +109,7 @@ public class HmaTests series.Add(bar.Time, bar.Close); } - var seriesResults = Hma.Calculate(series, 14); + var seriesResults = Hma.Batch(series, 14); double[] input = series.Values.ToArray(); double[] output = new double[input.Length]; @@ -247,7 +247,7 @@ public class HmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Hma.Calculate(series, period); + var batchSeries = Hma.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/hma/Hma.cs b/lib/trends/hma/Hma.cs index 8edf38f8..f5b78c14 100644 --- a/lib/trends/hma/Hma.cs +++ b/lib/trends/hma/Hma.cs @@ -20,7 +20,7 @@ namespace QuanTAlib; /// https://alan.hull.com.au/hma.html /// [SkipLocalsInit] -public sealed class Hma : ITValuePublisher +public sealed class Hma : AbstractBase { private readonly int _period; private readonly int _sqrtPeriod; @@ -29,10 +29,7 @@ public sealed class Hma : ITValuePublisher private readonly Wma _wmaSqrt; private int _sampleCount; - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _sampleCount >= _period + _sqrtPeriod - 1; - public event Action? Pub; + public override bool IsHot => _sampleCount >= WarmupPeriod; public Hma(int period) { @@ -47,6 +44,7 @@ public sealed class Hma : ITValuePublisher _wmaSqrt = new Wma(_sqrtPeriod); Name = $"Hma({period})"; + WarmupPeriod = period + _sqrtPeriod - 1; // WMA needs period, then WMA(sqrt) needs sqrt_period. Total lag/warmup. } public Hma(ITValuePublisher source, int period) : this(period) @@ -55,7 +53,7 @@ public sealed class Hma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) _sampleCount++; @@ -71,13 +69,13 @@ public sealed class Hma : ITValuePublisher // 4. Calculate HMA = WMA(sqrt(n), intermediate) Last = _wmaSqrt.Update(new TValue(input.Time, intermediate), isNew); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { - if (source.Count == 0) return new TSeries([], []); + if (source.Count == 0) return []; int len = source.Count; var t = new List(len); @@ -92,23 +90,37 @@ public sealed class Hma : ITValuePublisher source.Times.CopyTo(tSpan); // Restore state for streaming - _wmaFull.Reset(); - _wmaHalf.Reset(); - _wmaSqrt.Reset(); + Reset(); - int lookback = _period + (int)Math.Sqrt(_period) + 10; // Sufficient lookback + // We need to replay enough history to get the state right. + // HMA depends on 3 WMAs. + // WMA state depends on the last 'period' values. + // So we need to replay at least _period + _sqrtPeriod + buffer. + int lookback = _period + _sqrtPeriod + 10; int startIndex = Math.Max(0, len - lookback); - _sampleCount = startIndex; + + // We can't easily set _sampleCount without replaying, or we assume it's just count. + // But WMA internal state needs to be restored. + // Since WMA doesn't expose Prime/State easily (unless we cast and check), replaying is safer. for (int i = startIndex; i < len; i++) { - Update(source[i]); + Update(new TValue(source.Times[i], source.Values[i])); } + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period) { int len = source.Count; var t = new List(len); @@ -142,15 +154,24 @@ public sealed class Hma : ITValuePublisher double[] rentedHalf = System.Buffers.ArrayPool.Shared.Rent(len); Span halfWma = rentedHalf.AsSpan(0, len); - // Reuse halfWma buffer for intermediate results + // Reuse halfWma buffer for intermediate results to save memory/allocations + // But we need halfWma values for the calculation. + // Wait, CalculateIntermediate reads halfWma and fullWma and writes to output. + // So we can write to 'halfWma' IF we don't need 'halfWma' anymore. + // CalculateIntermediate iterates. If we write to halfWma in place, we overwrite values we might need if we were doing something else. + // But here: output[i] = 2*half[i] - full[i]. + // This is element-wise. So we CAN overwrite half[i] with the result if we process carefully or if we don't need half[i] later. + // We don't need half[i] later. + // So we can use halfWma as the intermediate buffer. + Span intermediate = halfWma; try { - Wma.Calculate(source, fullWma, period); - Wma.Calculate(source, halfWma, halfPeriod); + Wma.Batch(source, fullWma, period); + Wma.Batch(source, halfWma, halfPeriod); CalculateIntermediate(halfWma, fullWma, intermediate); - Wma.Calculate(intermediate, output, sqrtPeriod); + Wma.Batch(intermediate, output, sqrtPeriod); } finally { @@ -209,7 +230,7 @@ public sealed class Hma : ITValuePublisher } } - public void Reset() + public override void Reset() { _wmaFull.Reset(); _wmaHalf.Reset(); diff --git a/lib/trends/hma/Hma.md b/lib/trends/hma/Hma.md index 745fdfba..884e61e0 100644 --- a/lib/trends/hma/Hma.md +++ b/lib/trends/hma/Hma.md @@ -62,3 +62,40 @@ HMA can be used in various trading strategies: ## References * Hull, Alan. "Better Trading with the Hull Moving Average." MTA Symposium Proceedings, 2005 + +## C# Implementation + +### Standard Usage + +```csharp +using QuanTAlib; + +// Initialize with period 9 +var hma = new Hma(9); + +// Update with new value +TValue result = hma.Update(new TValue(time, price)); +Console.WriteLine($"HMA: {result.Value}"); +``` + +### Zero-Allocation Span API + +```csharp +double[] prices = ...; +double[] output = new double[prices.Length]; + +// Calculate HMA for the entire array +Hma.Batch(prices.AsSpan(), output.AsSpan(), period: 9); +``` + +### Bar Correction + +```csharp +var hma = new Hma(9); + +// Update with initial tick +hma.Update(new TValue(time, 100), isNew: true); + +// Update with correction (same bar) +hma.Update(new TValue(time, 101), isNew: false); +``` diff --git a/lib/trends/htit/Htit.Quantower.Tests.cs b/lib/trends/htit/Htit.Quantower.Tests.cs index bb4240e1..aaa32da1 100644 --- a/lib/trends/htit/Htit.Quantower.Tests.cs +++ b/lib/trends/htit/Htit.Quantower.Tests.cs @@ -28,7 +28,7 @@ public class HtitIndicatorTests { var time = DateTime.UtcNow.AddMinutes(i); indicator.HistoricalData.AddBar(time, 100 + i, 100 + i, 100 + i, 100 + i); - + var args = new UpdateArgs(UpdateReason.NewBar); indicator.ProcessUpdate(args); } diff --git a/lib/trends/htit/Htit.Quantower.cs b/lib/trends/htit/Htit.Quantower.cs index 92105249..106439fb 100644 --- a/lib/trends/htit/Htit.Quantower.cs +++ b/lib/trends/htit/Htit.Quantower.cs @@ -48,7 +48,7 @@ public class HtitIndicator : Indicator, IWatchlistIndicator { TValue input = this.GetInputValue(args, Source); bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; - + TValue result = _htit!.Update(input, isNew); Series!.SetValue(result.Value); Series!.SetMarker(0, Color.Transparent); diff --git a/lib/trends/htit/Htit.Tests.cs b/lib/trends/htit/Htit.Tests.cs index 7cc6a710..7576a321 100644 --- a/lib/trends/htit/Htit.Tests.cs +++ b/lib/trends/htit/Htit.Tests.cs @@ -165,7 +165,7 @@ public class HtitTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Htit.Calculate(series); + var batchSeries = Htit.Batch(series); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/htit/Htit.cs b/lib/trends/htit/Htit.cs index 9b70addc..f296210e 100644 --- a/lib/trends/htit/Htit.cs +++ b/lib/trends/htit/Htit.cs @@ -17,13 +17,8 @@ namespace QuanTAlib; /// https://dotnet.stockindicators.dev/indicators/HtTrendline/ /// [SkipLocalsInit] -public sealed class Htit : ITValuePublisher +public sealed class Htit : AbstractBase { - public string Name { get; } - public bool IsHot { get; private set; } - public event Action? Pub; - public TValue Last { get; private set; } - private readonly RingBuffer _priceBuffer; private readonly RingBuffer _smoothBuffer; private readonly RingBuffer _detrenderBuffer; @@ -37,9 +32,12 @@ public sealed class Htit : ITValuePublisher private State _state; private State _p_state; + public override bool IsHot => _priceBuffer.Count >= WarmupPeriod; + public Htit() { Name = "Htit"; + WarmupPeriod = 12; // Based on logic: _priceBuffer.Count >= 12 _priceBuffer = new RingBuffer(50); _smoothBuffer = new RingBuffer(7); _detrenderBuffer = new RingBuffer(7); @@ -56,7 +54,7 @@ public sealed class Htit : ITValuePublisher source.Pub += (item) => Update(item); } - public void Init() + private void Init() { _priceBuffer.Clear(); _smoothBuffer.Clear(); @@ -68,12 +66,11 @@ public sealed class Htit : ITValuePublisher _itBuffer.Clear(); _state = default; _p_state = default; - IsHot = false; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { ManageState(isNew); double price = ValidateInput(input.Value); @@ -123,13 +120,12 @@ public sealed class Htit : ITValuePublisher ? (4 * _itBuffer[^1] + 3 * _itBuffer[^2] + 2 * _itBuffer[^3] + _itBuffer[^4]) / 10.0 : price; - IsHot = _priceBuffer.Count >= 12; Last = new TValue(input.Time, trendline); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -157,6 +153,14 @@ public sealed class Htit : ITValuePublisher return new TSeries(t, v); } + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ManageState(bool isNew) { @@ -173,7 +177,7 @@ public sealed class Htit : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void UpdateBuffer(RingBuffer buffer, double val, bool isNew) + private static void UpdateBuffer(RingBuffer buffer, double val, bool isNew) { if (isNew) buffer.Add(val); else buffer.UpdateNewest(val); @@ -190,7 +194,7 @@ public sealed class Htit : ITValuePublisher UpdateBuffer(_itBuffer, price, isNew); Last = new TValue(input.Time, price); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } @@ -253,7 +257,7 @@ public sealed class Htit : ITValuePublisher return count > 0 ? sumPr / count : price; } - public static TSeries Calculate(TSeries source) + public static TSeries Batch(TSeries source) { var htit = new Htit(); return htit.Update(source); @@ -318,12 +322,12 @@ public sealed class Htit : ITValuePublisher // 2. Detrender double prevPeriod = periodBuffer[(pdIdx - 1 + 2) % 2]; double adj = (0.075 * prevPeriod) + 0.54; - + double s0 = smoothBuffer[sIdx]; double s2 = smoothBuffer[(sIdx - 2 + 7) % 7]; double s4 = smoothBuffer[(sIdx - 4 + 7) % 7]; double s6 = smoothBuffer[(sIdx - 6 + 7) % 7]; - + double detrender = (0.0962 * s0 + 0.5769 * s2 - 0.5769 * s4 - 0.0962 * s6) * adj; detrenderBuffer[dIdx] = detrender; @@ -332,10 +336,10 @@ public sealed class Htit : ITValuePublisher double d2 = detrenderBuffer[(dIdx - 2 + 7) % 7]; double d4 = detrenderBuffer[(dIdx - 4 + 7) % 7]; double d6 = detrenderBuffer[(dIdx - 6 + 7) % 7]; - + double q1 = (0.0962 * d0 + 0.5769 * d2 - 0.5769 * d4 - 0.0962 * d6) * adj; double i1 = detrenderBuffer[(dIdx - 3 + 7) % 7]; - + q1Buffer[q1Idx] = q1; i1Buffer[i1Idx] = i1; @@ -438,7 +442,7 @@ public sealed class Htit : ITValuePublisher } } - public void Reset() + public override void Reset() { Init(); } diff --git a/lib/trends/htit/Htit.md b/lib/trends/htit/Htit.md index a7ad139c..ec9977c3 100644 --- a/lib/trends/htit/Htit.md +++ b/lib/trends/htit/Htit.md @@ -47,12 +47,12 @@ TValue result = htit.Update(new TValue(time, price)); // Batch var series = new TSeries(times, prices); -var resultSeries = Htit.Calculate(series); +var resultSeries = Htit.Batch(series); // Span (Zero-Allocation) double[] input = ...; double[] output = new double[input.Length]; -Htit.Calculate(input, output); +Htit.Batch(input, output); ``` ## Interpretation diff --git a/lib/trends/jma/Jma.Quantower.Tests.cs b/lib/trends/jma/Jma.Quantower.Tests.cs index d6fd9a64..fc773630 100644 --- a/lib/trends/jma/Jma.Quantower.Tests.cs +++ b/lib/trends/jma/Jma.Quantower.Tests.cs @@ -120,7 +120,7 @@ public class JmaIndicatorTests { var indicator = new JmaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(JmaIndicator), method.DeclaringType); diff --git a/lib/trends/jma/Jma.Tests.cs b/lib/trends/jma/Jma.Tests.cs index 4ca12c4f..88ba0023 100644 --- a/lib/trends/jma/Jma.Tests.cs +++ b/lib/trends/jma/Jma.Tests.cs @@ -163,7 +163,7 @@ public class JmaTests } // Calculate with TSeries API - var tseriesResult = new Jma(10).Update(series); + var tseriesResult = Jma.Batch(series, 10); // Calculate with Span API Jma.Calculate(source.AsSpan(), output.AsSpan(), 10); @@ -185,7 +185,7 @@ public class JmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = new Jma(period).Update(series); + var batchSeries = Jma.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode @@ -229,9 +229,9 @@ public class JmaTests series.Add(bar.Time, bar.Close); } - var jmaPhase0 = new Jma(10, phase: 0).Update(series); - var jmaPhase100 = new Jma(10, phase: 100).Update(series); - var jmaPhaseMinus100 = new Jma(10, phase: -100).Update(series); + var jmaPhase0 = Jma.Batch(series, 10, phase: 0); + var jmaPhase100 = Jma.Batch(series, 10, phase: 100); + var jmaPhaseMinus100 = Jma.Batch(series, 10, phase: -100); Assert.NotEqual(jmaPhase0.Last.Value, jmaPhase100.Last.Value); Assert.NotEqual(jmaPhase0.Last.Value, jmaPhaseMinus100.Last.Value); diff --git a/lib/trends/jma/Jma.cs b/lib/trends/jma/Jma.cs index 64554423..43778ea5 100644 --- a/lib/trends/jma/Jma.cs +++ b/lib/trends/jma/Jma.cs @@ -13,7 +13,7 @@ namespace QuanTAlib; /// - Jurik dynamic exponent and 2-pole IIR core /// [SkipLocalsInit] -public sealed class Jma : ITValuePublisher +public sealed class Jma : AbstractBase { private const int VolWindowSize = 128; // volatility history length private const int DevWindowSize = 10; // short SMA length for deviation @@ -24,7 +24,6 @@ public sealed class Jma : ITValuePublisher private readonly double _lengthDivider; // L'/(L'+2), L' = 0.9*L private readonly double _logSqrtDivider; // Precomputed log(_sqrtDivider) for Exp optimization private readonly double _logLengthDivider; // Precomputed log(_lengthDivider) for Exp optimization - private readonly int _warmupBars; // for IsHot // Constants for trimmed mean private const int JurikTrimCount = 65; // canonical JMA: middle 65 of 128 samples @@ -57,15 +56,7 @@ public sealed class Jma : ITValuePublisher public int Bars; } - public string Name { get; } - public event Action? Pub; - public TValue Last { get; private set; } - - /// - /// JMA is considered "hot" when enough bars have passed to stabilize - /// the internal volatility distribution. - /// - public bool IsHot => _state.Bars >= _warmupBars; + public override bool IsHot => _state.Bars >= WarmupPeriod; public Jma(int period, int phase = 0, double power = 0.45) { @@ -100,7 +91,7 @@ public sealed class Jma : ITValuePublisher _logSqrtDivider = Math.Log(sqrtDivider); // same warmup heuristic used in the AFL port (SetBarsRequired) - _warmupBars = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36)); + WarmupPeriod = (int)Math.Ceiling(20.0 + 80.0 * Math.Pow(period, 0.36)); Name = $"Jma({period},{phase},{power})"; // power kept for signature compatibility @@ -118,7 +109,7 @@ public sealed class Jma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Reset() + public override void Reset() { _state = default; _p_state = default; @@ -232,45 +223,75 @@ public sealed class Jma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { double j = Step(input.Value, isNew); Last = new TValue(input.Time, j); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - /// - /// Batch update: recomputes JMA for entire series using the same - /// streaming core, so results match Update(TValue) applied bar-by-bar. - /// - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { - int n = source.Count; - if (n == 0) - return []; + if (source.Count == 0) return []; - var t = new List(n); - var v = new List(n); - - CollectionsMarshal.SetCount(t, n); - CollectionsMarshal.SetCount(v, n); + int len = source.Count; + var t = new List(len); + var v = new List(len); + CollectionsMarshal.SetCount(t, len); + CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); source.Times.CopyTo(tSpan); + // Use static Calculate for performance + // But JMA has complex parameters, so we need to pass them. + // We can use the instance to calculate, but we need to be careful about state. + // Or we can just loop using Step, which is what the original code did. + // Since JMA is complex and not easily vectorizable, looping is fine. + // But we should restore state afterwards. + + // RingBuffers are reference types, so we need to clone them or replay. + // Replaying is safer and cleaner for complex state. + Reset(); - for (int i = 0; i < n; i++) + for (int i = 0; i < len; i++) { double j = Step(source.Values[i], true); vSpan[i] = j; } + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); + + // Restore state by replaying history + // JMA needs a lot of history (128 bars for volatility). + Reset(); + int lookback = Math.Max(VolWindowSize + 10, WarmupPeriod + 10); + int startIndex = Math.Max(0, len - lookback); + for (int i = startIndex; i < len; i++) + { + Update(new TValue(source.Times[i], source.Values[i])); + } + return new TSeries(t, v); } + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period, int phase = 0, double power = 0.45) + { + var jma = new Jma(period, phase, power); + return jma.Update(source); + } + /// /// Static helper compatible with your existing signature. /// @@ -325,6 +346,6 @@ public sealed class Jma : ITValuePublisher if (end >= count) end = count - 1; int len = end - start + 1; - return _sorted.AsSpan(start, len).SumSIMD() / len; + return ((ReadOnlySpan)_sorted.AsSpan(start, len)).SumSIMD() / len; } } diff --git a/lib/trends/jma/Jma.md b/lib/trends/jma/Jma.md index 9d5f403d..d7637d4a 100644 --- a/lib/trends/jma/Jma.md +++ b/lib/trends/jma/Jma.md @@ -88,7 +88,7 @@ For high-performance batch processing: double[] prices = { 100.0, 101.5, 99.8, ... }; double[] output = new double[prices.Length]; -Jma.Calculate(prices, output, period: 10, phase: 0); +Jma.Batch(prices, output, period: 10, phase: 0); ``` ### Batch with TSeries diff --git a/lib/trends/kama/Kama.Tests.cs b/lib/trends/kama/Kama.Tests.cs index 7023fade..b627d4ac 100644 --- a/lib/trends/kama/Kama.Tests.cs +++ b/lib/trends/kama/Kama.Tests.cs @@ -245,7 +245,7 @@ public class KamaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Kama.Calculate(series, period); + var batchSeries = Kama.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/kama/Kama.cs b/lib/trends/kama/Kama.cs index 2158c1d2..c3de0918 100644 --- a/lib/trends/kama/Kama.cs +++ b/lib/trends/kama/Kama.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace QuanTAlib; @@ -18,7 +20,7 @@ namespace QuanTAlib; /// KAMA = KAMA[prev] + SC * (Price - KAMA[prev]) /// [SkipLocalsInit] -public sealed class Kama : ITValuePublisher +public sealed class Kama : AbstractBase { private readonly int _period; private readonly double _fastAlpha; @@ -29,22 +31,7 @@ public sealed class Kama : ITValuePublisher private State _state; private State _p_state; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; - - /// - /// Current KAMA value. - /// - public TValue Last { get; private set; } - - /// - /// True if the KAMA has enough data to produce valid results. - /// - public bool IsHot => _buffer.IsFull; + public override bool IsHot => _buffer.IsFull; /// /// Creates KAMA with specified parameters. @@ -72,6 +59,8 @@ public sealed class Kama : ITValuePublisher _slowAlpha = 2.0 / (slowPeriod + 1); Name = $"Kama({period}, {fastPeriod}, {slowPeriod})"; + WarmupPeriod = period + 1; + _state.Kama = double.NaN; _state.LastValidValue = double.NaN; _p_state.Kama = double.NaN; @@ -96,7 +85,7 @@ public sealed class Kama : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -111,7 +100,7 @@ public sealed class Kama : ITValuePublisher if (double.IsNaN(val)) { Last = new TValue(input.Time, double.NaN); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } @@ -125,7 +114,7 @@ public sealed class Kama : ITValuePublisher double diff_out = _p_state.NextDiffOut; double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); _state.VolatilitySum += diff_in - diff_out; - + // Calculate NextDiffOut for the next step // NextDiffOut = abs(buffer[0] - buffer[1]) _state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]); @@ -134,12 +123,12 @@ public sealed class Kama : ITValuePublisher { double diff_in = Math.Abs(_buffer[^1] - _buffer[^2]); _state.VolatilitySum += diff_in; - + if (_buffer.IsFull) { - // Buffer just became full. - // NextDiffOut = abs(buffer[0] - buffer[1]) - _state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]); + // Buffer just became full. + // NextDiffOut = abs(buffer[0] - buffer[1]) + _state.NextDiffOut = Math.Abs(_buffer[0] - _buffer[1]); } } } @@ -181,39 +170,38 @@ public sealed class Kama : ITValuePublisher double prevKama = _p_state.Kama; if (double.IsNaN(prevKama)) { - prevKama = _state.Kama; + prevKama = _state.Kama; } _state.Kama = prevKama + sc * (val - prevKama); } Last = new TValue(input.Time, _state.Kama); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return new TSeries([], []); int len = source.Count; var t = new List(len); var v = new List(len); + CollectionsMarshal.SetCount(t, len); + CollectionsMarshal.SetCount(v, len); + + var tSpan = CollectionsMarshal.AsSpan(t); + var vSpan = CollectionsMarshal.AsSpan(v); + + source.Times.CopyTo(tSpan); // Use static Calculate for performance - var outputSpan = new double[len]; - // fastPeriod = 2/fastAlpha - 1. int fastPeriod = (int)Math.Round(2.0 / _fastAlpha - 1); int slowPeriod = (int)Math.Round(2.0 / _slowAlpha - 1); - Calculate(source.Values, outputSpan, _period, fastPeriod, slowPeriod); - - for (int i = 0; i < len; i++) - { - t.Add(source.Times[i]); - v.Add(outputSpan[i]); - } + Calculate(source.Values, vSpan, _period, fastPeriod, slowPeriod); // Restore state by replaying the entire series // This is expensive but necessary to sync the object state correctly @@ -221,13 +209,22 @@ public sealed class Kama : ITValuePublisher Reset(); for (int i = 0; i < len; i++) { - Update(source[i]); + Update(new TValue(source.Times[i], source.Values[i])); } + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period, int fastPeriod = 2, int slowPeriod = 30) { var kama = new Kama(period, fastPeriod, slowPeriod); return kama.Update(source); @@ -326,7 +323,7 @@ public sealed class Kama : ITValuePublisher } } - public void Reset() + public override void Reset() { _buffer.Clear(); _state = default; diff --git a/lib/trends/kama/Kama.md b/lib/trends/kama/Kama.md index 8ef86312..e8bd6a2d 100644 --- a/lib/trends/kama/Kama.md +++ b/lib/trends/kama/Kama.md @@ -57,7 +57,7 @@ double[] prices = ...; double[] output = new double[prices.Length]; // Calculate KAMA for the entire array -Kama.Calculate(prices.AsSpan(), output.AsSpan(), period: 10, fastPeriod: 2, slowPeriod: 30); +Kama.Batch(prices.AsSpan(), output.AsSpan(), period: 10, fastPeriod: 2, slowPeriod: 30); ``` ### Bar Correction diff --git a/lib/trends/lsma/Lsma.Quantower.Tests.cs b/lib/trends/lsma/Lsma.Quantower.Tests.cs index 50f075ec..895d538b 100644 --- a/lib/trends/lsma/Lsma.Quantower.Tests.cs +++ b/lib/trends/lsma/Lsma.Quantower.Tests.cs @@ -118,7 +118,7 @@ public class LsmaIndicatorTests { var indicator = new LsmaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(LsmaIndicator), method.DeclaringType); diff --git a/lib/trends/lsma/Lsma.Tests.cs b/lib/trends/lsma/Lsma.Tests.cs index f3aa2bb1..1c10c766 100644 --- a/lib/trends/lsma/Lsma.Tests.cs +++ b/lib/trends/lsma/Lsma.Tests.cs @@ -141,7 +141,7 @@ public class LsmaTests var lsma = new Lsma(period); var series1 = lsma.Update(source); - var series2 = Lsma.Calculate(source, period); + var series2 = Lsma.Batch(source, period); Assert.Equal(series1.Count, series2.Count); for (int i = 0; i < count; i++) diff --git a/lib/trends/lsma/Lsma.cs b/lib/trends/lsma/Lsma.cs index efb891d4..2906c379 100644 --- a/lib/trends/lsma/Lsma.cs +++ b/lib/trends/lsma/Lsma.cs @@ -25,7 +25,7 @@ namespace QuanTAlib; /// Becomes true when the buffer is full (period samples processed). /// [SkipLocalsInit] -public sealed class Lsma : ITValuePublisher +public sealed class Lsma : AbstractBase { private readonly int _period; private readonly int _offset; @@ -37,17 +37,12 @@ public sealed class Lsma : ITValuePublisher private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue); private State _state; private State _p_state; - + private int _tickCount; private const int ResyncInterval = 1000; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; + public override bool IsHot => _buffer.IsFull; /// /// Creates LSMA with specified period and offset. @@ -63,14 +58,15 @@ public sealed class Lsma : ITValuePublisher _offset = offset; _buffer = new RingBuffer(period); Name = $"Lsma({period})"; + WarmupPeriod = period; // Precalculate constants // sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2 _sum_x = 0.5 * period * (period - 1); - + // sum_x2 = 0^2 + ... + (n-1)^2 = (n-1)n(2n-1)/6 double sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0; - + // denominator = n * sum_x2 - sum_x^2 _denominator = period * sum_x2 - _sum_x * _sum_x; } @@ -80,16 +76,6 @@ public sealed class Lsma : ITValuePublisher source.Pub += (item) => Update(item); } - /// - /// Current LSMA value. - /// - public TValue Last { get; private set; } - - /// - /// True if the LSMA has enough data to produce valid results. - /// - public bool IsHot => _buffer.IsFull; - [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { @@ -108,21 +94,21 @@ public sealed class Lsma : ITValuePublisher { double oldest = _buffer.Oldest; double prev_sum_y = _state.SumY; - + // O(1) update for sum_xy // sum_xy_new = sum_xy_old + sum_y_prev - n * oldest _state.SumXY = _state.SumXY + prev_sum_y - _period * oldest; - + // O(1) update for sum_y _state.SumY = _state.SumY - oldest + val; - + _buffer.Add(val); } else { _buffer.Add(val); _state.SumY += val; - + // Recalculate sum_xy from scratch during warmup _state.SumXY = 0; var span = _buffer.GetSpan(); @@ -158,7 +144,7 @@ public sealed class Lsma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -176,10 +162,10 @@ public sealed class Lsma : ITValuePublisher // For isNew=false, we update the current bar. // sum_xy remains constant because it depends on the previous window state which hasn't changed. // sum_y updates to reflect the change in the newest value. - + _state.SumY = _p_state.SumY - _p_state.LastVal + val; _state.SumXY = _p_state.SumXY; // Restore sum_xy to the state after the shift - + _buffer.UpdateNewest(val); _state.LastVal = val; } @@ -196,7 +182,7 @@ public sealed class Lsma : ITValuePublisher double n = _buffer.Count; double sx = _sum_x; double denom = _denominator; - + if (!_buffer.IsFull) { // Recalculate constants for smaller n @@ -213,20 +199,20 @@ public sealed class Lsma : ITValuePublisher { double m = (n * _state.SumXY - sx * _state.SumY) / denom; double b = (_state.SumY - m * sx) / n; - + // LSMA = b - m * offset result = b - m * _offset; } } Last = new TValue(input.Time, result); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var t = new List(len); @@ -279,10 +265,15 @@ public sealed class Lsma : ITValuePublisher return new TSeries(t, v); } - /// - /// Calculates LSMA for the entire series using a new instance. - /// - public static TSeries Calculate(TSeries source, int period, int offset = 0) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period, int offset = 0) { var lsma = new Lsma(period, offset); return lsma.Update(source); @@ -333,7 +324,7 @@ public sealed class Lsma : ITValuePublisher buffer[count] = val; sum_y += val; count++; - + // Recalculate sum_xy for current count sum_xy = 0; for (int j = 0; j < count; j++) @@ -365,7 +356,7 @@ public sealed class Lsma : ITValuePublisher output[i] = b - m * offset; } } - + if (count == period) { bufferIndex = 0; // Reset for circular buffer usage @@ -376,13 +367,13 @@ public sealed class Lsma : ITValuePublisher // Full buffer phase - O(1) update double oldest = buffer[bufferIndex]; double prev_sum_y = sum_y; - + // sum_xy_new = sum_xy_old + sum_y_prev - n * oldest sum_xy = sum_xy + prev_sum_y - period * oldest; - + sum_y = sum_y - oldest + val; buffer[bufferIndex] = val; - + bufferIndex++; if (bufferIndex >= period) bufferIndex = 0; @@ -397,7 +388,7 @@ public sealed class Lsma : ITValuePublisher /// /// Resets the LSMA state. /// - public void Reset() + public override void Reset() { _buffer.Clear(); _state = default; diff --git a/lib/trends/lsma/Lsma.md b/lib/trends/lsma/Lsma.md index a7b8fa4d..486e5a25 100644 --- a/lib/trends/lsma/Lsma.md +++ b/lib/trends/lsma/Lsma.md @@ -61,7 +61,7 @@ double[] input = { ... }; double[] output = new double[input.Length]; // Calculate LSMA in-place -Lsma.Calculate(input, output, period: 14); +Lsma.Batch(input, output, period: 14); ``` ### Bar Correction diff --git a/lib/trends/mama/Mama.Quantower.Tests.cs b/lib/trends/mama/Mama.Quantower.Tests.cs index c500b450..100c4c68 100644 --- a/lib/trends/mama/Mama.Quantower.Tests.cs +++ b/lib/trends/mama/Mama.Quantower.Tests.cs @@ -47,7 +47,7 @@ public class MamaIndicatorTests indicator.Initialize(); // After init, line series should exist (MAMA and FAMA) - Assert.Equal(2, indicator.LinesSeries.Length); + Assert.Equal(2, indicator.LinesSeries.Count); } [Fact] diff --git a/lib/trends/mama/Mama.Quantower.cs b/lib/trends/mama/Mama.Quantower.cs index c6c8b95e..0455224c 100644 --- a/lib/trends/mama/Mama.Quantower.cs +++ b/lib/trends/mama/Mama.Quantower.cs @@ -35,10 +35,10 @@ public class MamaIndicator : Indicator, IWatchlistIndicator SourceName = Source.ToString(); Name = "MAMA - MESA Adaptive Moving Average"; Description = "MESA Adaptive Moving Average"; - + MamaSeries = new(name: "MAMA", color: Color.Red, width: 2, style: LineStyle.Solid); FamaSeries = new(name: "FAMA", color: Color.Blue, width: 2, style: LineStyle.Solid); - + AddLineSeries(MamaSeries); AddLineSeries(FamaSeries); } @@ -55,12 +55,12 @@ public class MamaIndicator : Indicator, IWatchlistIndicator { TValue input = this.GetInputValue(args, Source); bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; - + TValue result = _ma!.Update(input, isNew); - + MamaSeries!.SetValue(result.Value); FamaSeries!.SetValue(_ma.Fama.Value); - + MamaSeries!.SetMarker(0, Color.Transparent); FamaSeries!.SetMarker(0, Color.Transparent); diff --git a/lib/trends/mama/Mama.Tests.cs b/lib/trends/mama/Mama.Tests.cs index d1491b8d..3523a7bd 100644 --- a/lib/trends/mama/Mama.Tests.cs +++ b/lib/trends/mama/Mama.Tests.cs @@ -170,7 +170,7 @@ public class MamaTests var mama = new Mama(); var series1 = mama.Update(source); - var series2 = Mama.Calculate(source); + var series2 = Mama.Batch(source); Assert.Equal(series1.Count, series2.Count); for (int i = 0; i < source.Count; i++) diff --git a/lib/trends/mama/Mama.cs b/lib/trends/mama/Mama.cs index eb10e8d5..ff87fcd5 100644 --- a/lib/trends/mama/Mama.cs +++ b/lib/trends/mama/Mama.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace QuanTAlib; @@ -8,12 +10,10 @@ namespace QuanTAlib; /// A trend-following indicator that adapts to the market's phase rate of change. /// [SkipLocalsInit] -public sealed class Mama : ITValuePublisher +public sealed class Mama : AbstractBase { - public TValue Last { get; private set; } public TValue Fama { get; private set; } - public bool IsHot => _state.Index > 6; - public event Action? Pub; + public override bool IsHot => _state.Index > 6; private readonly double _fastLimit; private readonly double _slowLimit; @@ -52,6 +52,7 @@ public sealed class Mama : ITValuePublisher _Q1_buffer = new RingBuffer(7); Name = $"Mama({fastLimit:F2},{slowLimit:F2})"; + WarmupPeriod = 7; Init(); } @@ -65,7 +66,7 @@ public sealed class Mama : ITValuePublisher Reset(); } - public void Reset() + public override void Reset() { _state = default; _state.Mama = double.NaN; @@ -83,7 +84,7 @@ public sealed class Mama : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + private double Step(double price, bool isNew) { if (isNew) { @@ -95,7 +96,6 @@ public sealed class Mama : ITValuePublisher _state = _p_state; } - double price = input.Value; if (!double.IsFinite(price)) { price = _state.LastValidPrice; @@ -186,7 +186,7 @@ public sealed class Mama : ITValuePublisher double avg = _state.Index > 0 ? _state.SumPr / _state.Index : price; _state.Mama = avg; _state.Fama = avg; - + // Initialize buffers with 0 _smoothBuffer.Add(0, isNew); _detrender.Add(0, isNew); @@ -194,15 +194,22 @@ public sealed class Mama : ITValuePublisher _Q1_buffer.Add(0, isNew); } - Last = new TValue(input.Time, _state.Mama); + return _state.Mama; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override TValue Update(TValue input, bool isNew = true) + { + double mama = Step(input.Value, isNew); + Last = new TValue(input.Time, mama); Fama = new TValue(input.Time, _state.Fama); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var v = new List(len); @@ -210,16 +217,23 @@ public sealed class Mama : ITValuePublisher for (int i = 0; i < len; i++) { - var item = source[i]; - var result = Update(item); + var result = Update(new TValue(source.Times[i], source.Values[i])); + t.Add(result.Time); v.Add(result.Value); - t.Add(item.Time); } return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Step(value, true); + } + } + + public static TSeries Batch(TSeries source, double fastLimit = 0.5, double slowLimit = 0.05) { var mama = new Mama(fastLimit, slowLimit); return mama.Update(source); @@ -227,12 +241,165 @@ public sealed class Mama : ITValuePublisher public static void Calculate(ReadOnlySpan source, Span output, double fastLimit = 0.5, double slowLimit = 0.05) { - var mama = new Mama(fastLimit, slowLimit); + if (source.Length == 0) return; + + // Stack allocate buffers for high performance (size 8 for power of 2 masking) + // We need 7 elements, but 8 allows & 7 masking + Span priceBuffer = stackalloc double[8]; + Span smoothBuffer = stackalloc double[8]; + Span detrender = stackalloc double[8]; + Span I1_buffer = stackalloc double[8]; + Span Q1_buffer = stackalloc double[8]; + + int bufferIdx = 0; // Current index for circular buffer + int count = 0; + + // State variables + double period = 0, mama = 0, sumPr = 0; + double i2 = 0, q2 = 0, re = 0, im = 0, lastValidPrice = 0; + double p_period = 0, p_phase = 0, p_mama = 0; + double p_i2 = 0, p_q2 = 0, p_re = 0, p_im = 0; + + // Constants + const int Mask = 7; + for (int i = 0; i < source.Length; i++) { - output[i] = mama.Update(new TValue(DateTime.MinValue, source[i])).Value; + double price = source[i]; + if (!double.IsFinite(price)) + { + price = count > 0 ? lastValidPrice : 0.0; + } + else + { + lastValidPrice = price; + } + + // Circular buffer update + bufferIdx = (bufferIdx + 1) & Mask; + priceBuffer[bufferIdx] = price; + count++; + + if (count > 6) + { + double adj = (0.075 * period) + 0.54; + + // Smooth + double smooth = (4.0 * priceBuffer[bufferIdx] + + 3.0 * priceBuffer[(bufferIdx - 1) & Mask] + + 2.0 * priceBuffer[(bufferIdx - 2) & Mask] + + priceBuffer[(bufferIdx - 3) & Mask]) * 0.1; + + smoothBuffer[bufferIdx] = smooth; + + // Detrender + double dt = (c1 * smoothBuffer[bufferIdx] + + c2 * smoothBuffer[(bufferIdx - 2) & Mask] - + c2 * smoothBuffer[(bufferIdx - 4) & Mask] - + c1 * smoothBuffer[(bufferIdx - 6) & Mask]) * adj; + + detrender[bufferIdx] = dt; + + // Q1 + double q1 = (c1 * dt + + c2 * detrender[(bufferIdx - 2) & Mask] - + c2 * detrender[(bufferIdx - 4) & Mask] - + c1 * detrender[(bufferIdx - 6) & Mask]) * adj; + + Q1_buffer[bufferIdx] = q1; + + // I1 = dt[3] + double i1 = detrender[(bufferIdx - 3) & Mask]; + I1_buffer[bufferIdx] = i1; + + // Advance phases + double jI = (c1 * i1 + + c2 * I1_buffer[(bufferIdx - 2) & Mask] - + c2 * I1_buffer[(bufferIdx - 4) & Mask] - + c1 * I1_buffer[(bufferIdx - 6) & Mask]) * adj; + + double jQ = (c1 * q1 + + c2 * Q1_buffer[(bufferIdx - 2) & Mask] - + c2 * Q1_buffer[(bufferIdx - 4) & Mask] - + c1 * Q1_buffer[(bufferIdx - 6) & Mask]) * adj; + + // Phasor addition + double i2_val = i1 - jQ; + double q2_val = q1 + jI; + + // Smooth i2, q2 + i2 = 0.2 * i2_val + 0.8 * p_i2; + q2 = 0.2 * q2_val + 0.8 * p_q2; + + // Homodyne discriminator + double re_val = (i2 * p_i2) + (q2 * p_q2); + double im_val = (i2 * p_q2) - (q2 * p_i2); + + // Smooth re, im + re = 0.2 * re_val + 0.8 * p_re; + im = 0.2 * im_val + 0.8 * p_im; + + // Calculate Period + double newPeriod = (Math.Abs(im) > double.Epsilon && Math.Abs(re) > double.Epsilon) + ? TWOPI / Math.Atan(im / re) + : 0.0; + + // Adjust Period + double periodCap = p_period * 1.5; + double periodFloor = p_period * 0.67; + + if (newPeriod > periodCap) newPeriod = periodCap; + if (newPeriod < periodFloor) newPeriod = periodFloor; + + if (newPeriod < 6.0) newPeriod = 6.0; + if (newPeriod > 50.0) newPeriod = 50.0; + + // Smooth Period + period = 0.2 * newPeriod + 0.8 * p_period; + + // Phase calculation + double phase = Math.Abs(i1) >= double.Epsilon ? Math.Atan(q1 / i1) * RadToDeg : 0.0; + + // Adaptive alpha + double delta = Math.Max(p_phase - phase, 1.0); + double alpha = fastLimit / delta; + alpha = Math.Clamp(alpha, slowLimit, fastLimit); + + // Final indicators + mama = alpha * priceBuffer[bufferIdx] + (1.0 - alpha) * p_mama; + + // Update previous state + p_i2 = i2; + p_q2 = q2; + p_re = re; + p_im = im; + p_period = period; + p_phase = phase; + p_mama = mama; + } + else + { + // Initialization + sumPr += price; + double avg = count > 0 ? sumPr / count : price; + mama = avg; + + // Init simple state + smoothBuffer[bufferIdx] = 0; + detrender[bufferIdx] = 0; + I1_buffer[bufferIdx] = 0; + Q1_buffer[bufferIdx] = 0; + + // Set initial p_state + p_mama = avg; + p_period = 0; // Initial period state + p_phase = 0; + + // Initialize other state variables if needed for next iteration logic? + // Actually they just stay 0/default until we hit count > 6 + } + + output[i] = mama; } } - - public string Name { get; set; } } diff --git a/lib/trends/mama/Mama.md b/lib/trends/mama/Mama.md index 7e226402..6407879b 100644 --- a/lib/trends/mama/Mama.md +++ b/lib/trends/mama/Mama.md @@ -62,6 +62,44 @@ MAMA is particularly valuable for identifying trends in markets with varying cyc * **Mathematical complexity:** Requires proper implementation of digital signal processing concepts for accurate results * **Complementary tools:** Works best when combined with momentum indicators or volume analysis for confirmation +## C# Implementation + +### Standard Usage + +```csharp +using QuanTAlib; + +// Create MAMA with default parameters +var mama = new Mama(fastLimit: 0.5, slowLimit: 0.05); + +// Update with new price +var result = mama.Update(new TValue(DateTime.UtcNow, 100.0)); +Console.WriteLine($"MAMA: {result.Value}"); +Console.WriteLine($"FAMA: {mama.Fama.Value}"); +``` + +### Static API (High Performance) + +```csharp +// Calculate MAMA for an entire array +double[] prices = { ... }; +double[] results = new double[prices.Length]; + +Mama.Batch(prices, results, fastLimit: 0.5, slowLimit: 0.05); +``` + +### Event-Driven + +```csharp +var source = new TSeries(); +var mama = new Mama(source); + +mama.Pub += (item) => { + Console.WriteLine($"MAMA: {item.Value}"); + Console.WriteLine($"FAMA: {mama.Fama.Value}"); +}; +``` + ## References 1. Ehlers, J. (2001). *MESA and Trading Market Cycles*. John Wiley & Sons. diff --git a/lib/trends/mgdi/Mgdi.Quantower.Tests.cs b/lib/trends/mgdi/Mgdi.Quantower.Tests.cs index 186e16e1..7bdf2fe9 100644 --- a/lib/trends/mgdi/Mgdi.Quantower.Tests.cs +++ b/lib/trends/mgdi/Mgdi.Quantower.Tests.cs @@ -28,7 +28,7 @@ public class MgdiIndicatorTests { var time = DateTime.UtcNow.AddMinutes(i); indicator.HistoricalData.AddBar(time, 100 + i, 100 + i, 100 + i, 100 + i); - + var args = new UpdateArgs(UpdateReason.NewBar); indicator.ProcessUpdate(args); } diff --git a/lib/trends/mgdi/Mgdi.Quantower.cs b/lib/trends/mgdi/Mgdi.Quantower.cs index 1ee7b7d8..4d1a0756 100644 --- a/lib/trends/mgdi/Mgdi.Quantower.cs +++ b/lib/trends/mgdi/Mgdi.Quantower.cs @@ -51,7 +51,7 @@ public class MgdiIndicator : Indicator, IWatchlistIndicator { TValue input = this.GetInputValue(args, Source); bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar; - + TValue result = _mgdi!.Update(input, isNew); Series!.SetValue(result.Value); Series!.SetMarker(0, Color.Transparent); diff --git a/lib/trends/mgdi/Mgdi.Tests.cs b/lib/trends/mgdi/Mgdi.Tests.cs index dc39c03f..e7470418 100644 --- a/lib/trends/mgdi/Mgdi.Tests.cs +++ b/lib/trends/mgdi/Mgdi.Tests.cs @@ -34,7 +34,7 @@ public class MgdiTests var data = _gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)).Close; var series = data; - var resultSeries = mgdi.Update(series); + var resultSeries = Mgdi.Batch(series); // Reset and calculate streaming mgdi.Reset(); diff --git a/lib/trends/mgdi/Mgdi.cs b/lib/trends/mgdi/Mgdi.cs index e89bfe51..7dcbc597 100644 --- a/lib/trends/mgdi/Mgdi.cs +++ b/lib/trends/mgdi/Mgdi.cs @@ -20,20 +20,17 @@ namespace QuanTAlib; /// Default k = 0.6 /// [SkipLocalsInit] -public sealed class Mgdi : ITValuePublisher +public sealed class Mgdi : AbstractBase { - public string Name { get; } - public bool IsHot { get; private set; } - public event Action? Pub; - public TValue Last { get; private set; } - private readonly int _period; private readonly double _k; - + private record struct State(double LastMgdi, double LastValidValue, int Count); private State _state; private State _p_state; + public override bool IsHot => _state.Count >= _period; + public Mgdi(int period = 14, double k = 0.6) { if (period < 1) throw new ArgumentOutOfRangeException(nameof(period)); @@ -41,6 +38,7 @@ public sealed class Mgdi : ITValuePublisher _period = period; _k = k; Name = $"Mgdi({period},{k})"; + WarmupPeriod = period; Init(); } @@ -53,12 +51,11 @@ public sealed class Mgdi : ITValuePublisher { _state = default; _p_state = default; - IsHot = false; Last = default; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) _p_state = _state; else _state = _p_state; @@ -87,25 +84,24 @@ public sealed class Mgdi : ITValuePublisher double ratio = price / prev; double ratio4 = ratio * ratio; ratio4 *= ratio4; - + double denominator = _k * _period * ratio4; _state.LastMgdi = prev + (price - prev) / denominator; } else { - _state.LastMgdi = price; + _state.LastMgdi = price; } } - IsHot = _state.Count >= _period; Last = new TValue(input.Time, _state.LastMgdi); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) return new TSeries([], []); int len = source.Count; var t = new List(len); @@ -121,9 +117,8 @@ public sealed class Mgdi : ITValuePublisher // Restore state Init(); - // Replay last portion to restore state - int startIndex = Math.Max(0, len - Math.Max(_period * 2, 100)); - for (int i = startIndex; i < len; i++) + // Replay the whole series to restore state correctly as it is recursive + for (int i = 0; i < len; i++) { Update(new TValue(source.Times[i], source.Values[i])); } @@ -132,7 +127,15 @@ public sealed class Mgdi : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period = 14, double k = 0.6) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period = 14, double k = 0.6) { var mgdi = new Mgdi(period, k); return mgdi.Update(source); @@ -161,7 +164,7 @@ public sealed class Mgdi : ITValuePublisher double ratio = price / lastMgdi; double ratio4 = ratio * ratio; ratio4 *= ratio4; - + double denominator = k * period * ratio4; lastMgdi += (price - lastMgdi) / denominator; } @@ -169,12 +172,12 @@ public sealed class Mgdi : ITValuePublisher { lastMgdi = price; } - + output[i] = lastMgdi; } } - public void Reset() + public override void Reset() { Init(); } diff --git a/lib/trends/mgdi/Mgdi.md b/lib/trends/mgdi/Mgdi.md index 8fe0095e..b29313a3 100644 --- a/lib/trends/mgdi/Mgdi.md +++ b/lib/trends/mgdi/Mgdi.md @@ -58,7 +58,7 @@ double[] input = { ... }; // Your price data double[] output = new double[input.Length]; // Calculate MGDI over the entire span -Mgdi.Calculate(input, output, period: 14, k: 0.6); +Mgdi.Batch(input, output, period: 14, k: 0.6); ``` ### Event-Driven Usage diff --git a/lib/trends/pwma/Pwma.Quantower.Tests.cs b/lib/trends/pwma/Pwma.Quantower.Tests.cs index 7056ab98..0fd039c8 100644 --- a/lib/trends/pwma/Pwma.Quantower.Tests.cs +++ b/lib/trends/pwma/Pwma.Quantower.Tests.cs @@ -118,7 +118,7 @@ public class PwmaIndicatorTests { var indicator = new PwmaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(PwmaIndicator), method.DeclaringType); diff --git a/lib/trends/pwma/Pwma.Tests.cs b/lib/trends/pwma/Pwma.Tests.cs index 1b7c4788..fe190a78 100644 --- a/lib/trends/pwma/Pwma.Tests.cs +++ b/lib/trends/pwma/Pwma.Tests.cs @@ -187,7 +187,6 @@ public class PwmaTests public void Pwma_BatchCalc_MatchesIterativeCalc() { var pwmaIterative = new Pwma(10); - var pwmaBatch = new Pwma(10); var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1); // Generate data @@ -208,7 +207,7 @@ public class PwmaTests } // Calculate batch - var batchResults = pwmaBatch.Update(series); + var batchResults = Pwma.Batch(series, 10); // Compare Assert.Equal(iterativeResults.Count, batchResults.Count); @@ -262,7 +261,7 @@ public class PwmaTests series.Add(DateTime.UtcNow.Ticks + 1, 20); series.Add(DateTime.UtcNow.Ticks + 2, 30); - var results = Pwma.Calculate(series, 3); + var results = Pwma.Batch(series, 3); Assert.Equal(3, results.Count); // PWMA(3) for last 3 values [10,20,30]: 360/14 @@ -324,7 +323,7 @@ public class PwmaTests } // Calculate with TSeries API - var tseriesResult = Pwma.Calculate(series, 10); + var tseriesResult = Pwma.Batch(series, 10); // Calculate with Span API Pwma.Calculate(source.AsSpan(), output.AsSpan(), 10); @@ -363,7 +362,7 @@ public class PwmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Pwma.Calculate(series, period); + var batchSeries = Pwma.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode diff --git a/lib/trends/pwma/Pwma.cs b/lib/trends/pwma/Pwma.cs index 7f0417bd..95bed2ac 100644 --- a/lib/trends/pwma/Pwma.cs +++ b/lib/trends/pwma/Pwma.cs @@ -27,7 +27,7 @@ namespace QuanTAlib; /// S3 is parabolic weighted sum /// [SkipLocalsInit] -public sealed class Pwma : ITValuePublisher +public sealed class Pwma : AbstractBase { private readonly int _period; private readonly double _divisor; @@ -39,10 +39,7 @@ public sealed class Pwma : ITValuePublisher private const int ResyncInterval = 1000; - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _buffer.IsFull; - public event Action? Pub; + public override bool IsHot => _buffer.IsFull; public Pwma(int period) { @@ -52,6 +49,7 @@ public sealed class Pwma : ITValuePublisher _divisor = (double)period * (period + 1) * (2 * period + 1) / 6.0; _buffer = new RingBuffer(period); Name = $"Pwma({period})"; + WarmupPeriod = period; } public Pwma(ITValuePublisher source, int period) : this(period) @@ -115,7 +113,7 @@ public sealed class Pwma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -136,10 +134,10 @@ public sealed class Pwma : ITValuePublisher // S1' = S1 - last + new // S2' = S2 - n*last + n*new // S3' = S3 - n^2*last + n^2*new - + int n = _buffer.IsFull ? _period : _buffer.Count; double diff = val - _state.LastInput; - + _state.Sum += diff; _state.WSum += n * diff; _state.PSum += (double)n * n * diff; @@ -149,13 +147,13 @@ public sealed class Pwma : ITValuePublisher double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * (2 * _buffer.Count + 1) / 6.0; Last = new TValue(input.Time, _state.PSum / currentDivisor); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { - if (source.Count == 0) return []; + if (source.Count == 0) return new TSeries([], []); int len = source.Count; List t = new(len); @@ -165,7 +163,7 @@ public sealed class Pwma : ITValuePublisher var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - + Calculate(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); @@ -209,7 +207,15 @@ public sealed class Pwma : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public override void Prime(ReadOnlySpan source) + { + foreach (var value in source) + { + Update(new TValue(DateTime.MinValue, value)); + } + } + + public static TSeries Batch(TSeries source, int period) { var pwma = new Pwma(period); return pwma.Update(source); @@ -290,12 +296,12 @@ public sealed class Pwma : ITValuePublisher double recalcSum = 0; double recalcWsum = 0; double recalcPsum = 0; - + for (int k = 0; k < period; k++) { int idx = bufferIdx + k; if (idx >= period) idx -= period; - + double v = buffer[idx]; recalcSum += v; recalcWsum += (k + 1) * v; @@ -310,7 +316,7 @@ public sealed class Pwma : ITValuePublisher } } - public void Reset() + public override void Reset() { _buffer.Clear(); _state = default; diff --git a/lib/trends/pwma/Pwma.md b/lib/trends/pwma/Pwma.md index 35a0c10e..8a8543a5 100644 --- a/lib/trends/pwma/Pwma.md +++ b/lib/trends/pwma/Pwma.md @@ -79,12 +79,12 @@ Console.WriteLine($"IsHot: {pwma.IsHot}"); // true when buffer is full // Batch calculation (TSeries API) TSeries source = ...; -TSeries results = Pwma.Calculate(source, 14); +TSeries results = Pwma.Batch(source, 14); // High-performance Span API (zero allocation) double[] prices = new double[10000]; double[] output = new double[10000]; -Pwma.Calculate(prices.AsSpan(), output.AsSpan(), period: 14); +Pwma.Batch(prices.AsSpan(), output.AsSpan(), period: 14); ``` ### Zero-Allocation Span API @@ -97,7 +97,7 @@ double[] source = new double[200000]; double[] pwmaOutput = new double[200000]; // Zero heap allocation during calculation -Pwma.Calculate(source.AsSpan(), pwmaOutput.AsSpan(), period: 100); +Pwma.Batch(source.AsSpan(), pwmaOutput.AsSpan(), period: 100); // Results are written directly to output buffer Console.WriteLine($"Last PWMA: {pwmaOutput[^1]}"); diff --git a/lib/trends/rma/Rma.Tests.cs b/lib/trends/rma/Rma.Tests.cs index e15e9aba..a56a0de7 100644 --- a/lib/trends/rma/Rma.Tests.cs +++ b/lib/trends/rma/Rma.Tests.cs @@ -175,10 +175,10 @@ public class RmaTests } // Calculate with TSeries API - var tseriesResult = Rma.Calculate(series, 10); + var tseriesResult = Rma.Batch(series, 10); // Calculate with Span API - Rma.Calculate(source.AsSpan(), output.AsSpan(), 10); + Rma.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) diff --git a/lib/trends/rma/Rma.cs b/lib/trends/rma/Rma.cs index 2f9593c5..48767948 100644 --- a/lib/trends/rma/Rma.cs +++ b/lib/trends/rma/Rma.cs @@ -17,17 +17,9 @@ namespace QuanTAlib; /// utilizing the same O(1) update complexity and zero-allocation architecture. /// [SkipLocalsInit] -public sealed class Rma : ITValuePublisher +public sealed class Rma : AbstractBase { private readonly Ema _ema; - private readonly int _period; - - /// - /// Display name for the indicator. - /// - public string Name => $"Rma({_period})"; - - public event Action? Pub; /// /// Creates RMA with specified period. @@ -39,9 +31,9 @@ public sealed class Rma : ITValuePublisher if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); - _period = period; _ema = new Ema(1.0 / period); - _ema.Pub += (item) => Pub?.Invoke(item); + Name = $"Rma({period})"; + WarmupPeriod = _ema.WarmupPeriod; } /// @@ -56,24 +48,49 @@ public sealed class Rma : ITValuePublisher } /// - /// Current RMA value. + /// Creates RMA with specified source and period. /// - public TValue Last => _ema.Last; + /// Source series + /// Period for RMA calculation + public Rma(TSeries source, int period) : this(period) + { + Prime(source.Values); + if (source.Count > 0) + { + Last = new TValue(source.LastTime, Last.Value); + } + source.Pub += (item) => Update(item); + } /// /// True if the RMA has warmed up and is providing valid results. /// - public bool IsHot => _ema.IsHot; + public override bool IsHot => _ema.IsHot; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + /// + /// Initializes the indicator state using the provided history. + /// + /// Historical data + public override void Prime(ReadOnlySpan source) { - return _ema.Update(input, isNew); + _ema.Prime(source); + Last = _ema.Last; } - public TSeries Update(TSeries source) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override TValue Update(TValue input, bool isNew = true) { - return _ema.Update(source); + TValue result = _ema.Update(input, isNew); + Last = result; + PubEvent(Last); + return result; + } + + public override TSeries Update(TSeries source) + { + TSeries result = _ema.Update(source); + Last = _ema.Last; + return result; } /// @@ -82,7 +99,7 @@ public sealed class Rma : ITValuePublisher /// Input series /// RMA period /// RMA series - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var rma = new Rma(period); return rma.Update(source); @@ -97,20 +114,35 @@ public sealed class Rma : ITValuePublisher /// Output span (must be same length as source) /// RMA period (must be > 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); double alpha = 1.0 / period; - Ema.Calculate(source, output, alpha); + Ema.Batch(source, output, alpha); + } + + /// + /// Runs a high-performance batch calculation on history and returns + /// a "Hot" Rma instance ready to process the next tick immediately. + /// + /// Historical time series + /// RMA Period + /// A tuple containing the full calculation results and the hot indicator instance + public static (TSeries Results, Rma Indicator) Calculate(TSeries source, int period) + { + var rma = new Rma(period); + TSeries results = rma.Update(source); + return (results, rma); } /// /// Resets the RMA state. /// - public void Reset() + public override void Reset() { _ema.Reset(); + Last = default; } } diff --git a/lib/trends/rma/Rma.md b/lib/trends/rma/Rma.md index 5e518540..596ee232 100644 --- a/lib/trends/rma/Rma.md +++ b/lib/trends/rma/Rma.md @@ -65,7 +65,7 @@ double[] source = ...; double[] output = new double[source.Length]; // Zero-allocation calculation -Rma.Calculate(source, output, 14); +Rma.Batch(source, output, 14); ``` ### Event-Driven diff --git a/lib/trends/sma/Sma.Quantower.Tests.cs b/lib/trends/sma/Sma.Quantower.Tests.cs index 5a9ffe27..66405a91 100644 --- a/lib/trends/sma/Sma.Quantower.Tests.cs +++ b/lib/trends/sma/Sma.Quantower.Tests.cs @@ -116,7 +116,7 @@ public class SmaIndicatorTests { var indicator = new SmaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(SmaIndicator), method.DeclaringType); diff --git a/lib/trends/sma/Sma.Quantower.cs b/lib/trends/sma/Sma.Quantower.cs index 87413d99..c5bf525c 100644 --- a/lib/trends/sma/Sma.Quantower.cs +++ b/lib/trends/sma/Sma.Quantower.cs @@ -59,8 +59,12 @@ public class SmaIndicator : Indicator, IWatchlistIndicator public override void OnPaintChart(PaintChartEventArgs args) { + var savedColor = Series!.Color; + Series.Color = Color.Transparent; base.OnPaintChart(args); + Series.Color = savedColor; + int warmupPeriod = _warmupBarIndex > 0 ? _warmupBarIndex : Count; - this.PaintSmoothCurve(args, Series!, warmupPeriod, showColdValues: ShowColdValues, tension: 0.2); + this.PaintLine(args, Series!, warmupPeriod, showColdValues: ShowColdValues); } } diff --git a/lib/trends/sma/Sma.Tests.cs b/lib/trends/sma/Sma.Tests.cs index 432c2a67..fcabcae9 100644 --- a/lib/trends/sma/Sma.Tests.cs +++ b/lib/trends/sma/Sma.Tests.cs @@ -331,7 +331,7 @@ public class SmaTests } [Fact] - public void Sma_StaticCalculate_Works() + public void Sma_StaticBatch_Works() { var series = new TSeries(); series.Add(DateTime.UtcNow.Ticks, 10); @@ -340,7 +340,7 @@ public class SmaTests series.Add(DateTime.UtcNow.Ticks + 3, 40); series.Add(DateTime.UtcNow.Ticks + 4, 50); - var results = Sma.Calculate(series, 3); + var results = Sma.Batch(series, 3); Assert.Equal(5, results.Count); // SMA(3) for last value: (30+40+50)/3 = 40 @@ -360,22 +360,22 @@ public class SmaTests // ============== Span API Tests ============== [Fact] - public void Sma_SpanCalc_ValidatesInput() + public void Sma_SpanBatch_ValidatesInput() { double[] source = [1, 2, 3, 4, 5]; double[] output = new double[5]; double[] wrongSizeOutput = new double[3]; // Period must be > 0 - Assert.Throws(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), 0)); - Assert.Throws(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), -1)); + Assert.Throws(() => Sma.Batch(source.AsSpan(), output.AsSpan(), 0)); + Assert.Throws(() => Sma.Batch(source.AsSpan(), output.AsSpan(), -1)); // Output must be same length as source - Assert.Throws(() => Sma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); + Assert.Throws(() => Sma.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3)); } [Fact] - public void Sma_SpanCalc_MatchesTSeriesCalc() + public void Sma_SpanBatch_MatchesTSeriesBatch() { var series = new TSeries(); double[] source = new double[100]; @@ -390,10 +390,10 @@ public class SmaTests } // Calculate with TSeries API - var tseriesResult = Sma.Calculate(series, 10); + var tseriesResult = Sma.Batch(series, 10); // Calculate with Span API - Sma.Calculate(source.AsSpan(), output.AsSpan(), 10); + Sma.Batch(source.AsSpan(), output.AsSpan(), 10); // Compare results for (int i = 0; i < 100; i++) @@ -403,12 +403,12 @@ public class SmaTests } [Fact] - public void Sma_SpanCalc_CalculatesCorrectly() + public void Sma_SpanBatch_CalculatesCorrectly() { double[] source = [10, 20, 30, 40, 50]; double[] output = new double[5]; - Sma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Sma.Batch(source.AsSpan(), output.AsSpan(), 3); // SMA(3) warmup: 10, (10+20)/2=15, (10+20+30)/3=20, then sliding: (20+30+40)/3=30, (30+40+50)/3=40 Assert.Equal(10.0, output[0], 1e-10); @@ -419,7 +419,7 @@ public class SmaTests } [Fact] - public void Sma_SpanCalc_ZeroAllocation() + public void Sma_SpanBatch_ZeroAllocation() { double[] source = new double[10000]; @@ -429,7 +429,7 @@ public class SmaTests source[i] = gbm.Next().Close; // Warm up - Sma.Calculate(source.AsSpan(), output.AsSpan(), 100); + Sma.Batch(source.AsSpan(), output.AsSpan(), 100); // This test verifies the method runs without throwing // (allocation is measured by BenchmarkDotNet, not unit tests) @@ -437,12 +437,12 @@ public class SmaTests } [Fact] - public void Sma_SpanCalc_HandlesNaN() + public void Sma_SpanBatch_HandlesNaN() { double[] source = [100, 110, double.NaN, 120, 130]; double[] output = new double[5]; - Sma.Calculate(source.AsSpan(), output.AsSpan(), 3); + Sma.Batch(source.AsSpan(), output.AsSpan(), 3); // All outputs should be finite foreach (var val in output) @@ -452,12 +452,12 @@ public class SmaTests } [Fact] - public void Sma_SpanCalc_Period1_ReturnsInput() + public void Sma_SpanBatch_Period1_ReturnsInput() { double[] source = [10, 20, 30, 40, 50]; double[] output = new double[5]; - Sma.Calculate(source.AsSpan(), output.AsSpan(), 1); + Sma.Batch(source.AsSpan(), output.AsSpan(), 1); for (int i = 0; i < source.Length; i++) { @@ -474,14 +474,14 @@ public class SmaTests var series = bars.Close; // 1. Batch Mode - var batchSeries = Sma.Calculate(series, period); + var batchSeries = Sma.Batch(series, period); double expected = batchSeries.Last.Value; // 2. Span Mode var tValues = series.Values.ToArray(); var spanInput = new ReadOnlySpan(tValues); var spanOutput = new double[tValues.Length]; - Sma.Calculate(spanInput, spanOutput, period); + Sma.Batch(spanInput, spanOutput, period); double spanResult = spanOutput[^1]; // 3. Streaming Mode @@ -516,4 +516,82 @@ public class SmaTests source.Add(new TValue(DateTime.UtcNow, 100)); Assert.Equal(100, sma.Last.Value); } + + [Fact] + public void WarmupPeriod_IsSetCorrectly() + { + var sma = new Sma(10); + Assert.Equal(10, sma.WarmupPeriod); + } + + [Fact] + public void Prime_SetsStateCorrectly() + { + var sma = new Sma(5); + double[] history = [10, 20, 30, 40, 50]; // SMA(5) = 30 + + sma.Prime(history); + + Assert.True(sma.IsHot); + Assert.Equal(30.0, sma.Last.Value, 1e-10); + + // Verify it continues correctly + sma.Update(new TValue(DateTime.UtcNow, 60)); // 20,30,40,50,60 -> 40 + Assert.Equal(40.0, sma.Last.Value, 1e-10); + } + + [Fact] + public void Prime_WithInsufficientHistory_IsNotHot() + { + var sma = new Sma(10); + double[] history = [10, 20, 30, 40, 50]; + + sma.Prime(history); + + Assert.False(sma.IsHot); + Assert.Equal(30.0, sma.Last.Value, 1e-10); // It still calculates what it can + } + + [Fact] + public void Prime_HandlesNaN_InHistory() + { + var sma = new Sma(3); + double[] history = [10, 20, double.NaN, 40]; + // 10 + // 10, 20 + // 10, 20, 20 (NaN replaced by 20) -> Avg(10,20,20) = 16.666... + // 20, 20, 40 -> Avg(20,20,40) = 26.666... + + sma.Prime(history); + + Assert.True(sma.IsHot); + Assert.Equal(80.0 / 3.0, sma.Last.Value, 1e-9); + } + + [Fact] + public void Calculate_ReturnsCorrectResultsAndHotIndicator() + { + var series = new TSeries(); + for (int i = 1; i <= 10; i++) series.Add(DateTime.UtcNow, i * 10); + // 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 + + // SMA(5) + var (results, indicator) = Sma.Calculate(series, 5); + + // Check results + Assert.Equal(10, results.Count); + Assert.Equal(30.0, results[4].Value); // 5th element (index 4) is SMA(10..50) = 30 + Assert.Equal(80.0, results.Last.Value); // Last element is SMA(60..100) = 80 + + // Check indicator state + Assert.True(indicator.IsHot); + Assert.Equal(80.0, indicator.Last.Value); + Assert.Equal(5, indicator.WarmupPeriod); + + // Verify indicator continues correctly + indicator.Update(new TValue(DateTime.UtcNow, 110)); + // Window was [60, 70, 80, 90, 100] -> Avg 80 + // New Window [70, 80, 90, 100, 110] -> Avg 90 + Assert.Equal(90.0, indicator.Last.Value); + } } diff --git a/lib/trends/sma/Sma.Validation.Tests.cs b/lib/trends/sma/Sma.Validation.Tests.cs index 82eee196..0bdd4d91 100644 --- a/lib/trends/sma/Sma.Validation.Tests.cs +++ b/lib/trends/sma/Sma.Validation.Tests.cs @@ -91,7 +91,7 @@ public class SmaValidationTests : IDisposable { // Calculate QuanTAlib SMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Sma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate Skender SMA var sResult = _testData.SkenderQuotes.GetSma(period).ToList(); @@ -173,7 +173,7 @@ public class SmaValidationTests : IDisposable { // Calculate QuanTAlib SMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Sma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate TA-Lib SMA var retCode = TALib.Functions.Sma(sourceData, 0..^0, talibOutput, out var outRange, period); @@ -263,7 +263,7 @@ public class SmaValidationTests : IDisposable { // Calculate QuanTAlib SMA (Span API) double[] qOutput = new double[sourceData.Length]; - global::QuanTAlib.Sma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period); + global::QuanTAlib.Sma.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period); // Calculate Tulip SMA var smaIndicator = Tulip.Indicators.sma; diff --git a/lib/trends/sma/Sma.cs b/lib/trends/sma/Sma.cs index 34d0a429..9cf18632 100644 --- a/lib/trends/sma/Sma.cs +++ b/lib/trends/sma/Sma.cs @@ -26,7 +26,7 @@ namespace QuanTAlib; /// Becomes true when the buffer is full (period samples processed). /// [SkipLocalsInit] -public sealed class Sma : ITValuePublisher +public sealed class Sma : AbstractBase { private readonly int _period; private readonly RingBuffer _buffer; @@ -37,13 +37,6 @@ public sealed class Sma : ITValuePublisher private const int ResyncInterval = 1000; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; - /// /// Creates SMA with specified period. /// @@ -56,6 +49,7 @@ public sealed class Sma : ITValuePublisher _period = period; _buffer = new RingBuffer(period); Name = $"Sma({period})"; + WarmupPeriod = period; } public Sma(ITValuePublisher source, int period) : this(period) @@ -63,16 +57,93 @@ public sealed class Sma : ITValuePublisher source.Pub += (item) => Update(item); } - /// - /// Current SMA value. - /// - public TValue Last { get; private set; } + public Sma(TSeries source, int period) : this(period) + { + Prime(source.Values); + if (source.Count > 0) + { + Last = new TValue(source.LastTime, Last.Value); + } + source.Pub += (item) => Update(item); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // Mode B: Streaming (Stateful) + ///////////////////////////////////////////////////////////////////////////////////////////////// /// /// True if the SMA has enough data to produce valid results. /// SMA is "hot" when the buffer is full (has received at least 'period' values). /// - public bool IsHot => _buffer.IsFull; + public override bool IsHot => _buffer.IsFull; + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // Mode C: Priming (The Bridge) + ///////////////////////////////////////////////////////////////////////////////////////////////// + + /// + /// Initializes the indicator state using the provided history. + /// Efficiently processes only the last 'Period' values required to sync the buffer. + /// + /// Historical data (only the last 'period' is actually needed) + public override void Prime(ReadOnlySpan source) + { + if (source.Length == 0) return; + + // Reset state + _buffer.Clear(); + _state = default; + _p_state = default; + + // We only need the last 'period' values to fully restore state + // If history is shorter than period, we take it all. + int warmupLength = Math.Min(source.Length, WarmupPeriod); + int startIndex = source.Length - warmupLength; + + // 1. Seed the LastValidValue (crucial for NaN handling) + // We must look backwards from start of our warmup window to find a valid predecessor + _state.LastValidValue = double.NaN; + for (int i = startIndex - 1; i >= 0; i--) + { + if (double.IsFinite(source[i])) + { + _state.LastValidValue = source[i]; + break; + } + } + + // If we didn't find a valid value in history, try finding one inside the warmup window + if (double.IsNaN(_state.LastValidValue)) + { + for (int i = startIndex; i < source.Length; i++) + { + if (double.IsFinite(source[i])) + { + _state.LastValidValue = source[i]; + break; + } + } + } + + // 2. Feed the RingBuffer and State + for (int i = startIndex; i < source.Length; i++) + { + double val = GetValidValue(source[i]); + UpdateState(val); + _state.LastInput = val; + } + + // 3. Finalize State + // Calculate the initial "Last" value so the indicator is ready to be read immediately + double result = _buffer.Count > 0 ? _state.Sum / _buffer.Count : double.NaN; + + // Note: We can't infer accurate Time from a simple Span, + // so we leave 'Last' with default time or user updates it on next Tick. + Last = new TValue(DateTime.MinValue, result); + + // Backup state for the next update cycle + _p_state = _state; + } /// /// Gets a valid input value, using last-value substitution for non-finite inputs. @@ -106,7 +177,7 @@ public sealed class Sma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -127,11 +198,11 @@ public sealed class Sma : ITValuePublisher double result = _state.Sum / _buffer.Count; Last = new TValue(input.Time, result); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -144,65 +215,26 @@ public sealed class Sma : ITValuePublisher var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - Calculate(source.Values, vSpan, _period); + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); - // Restore state - int windowSize = Math.Min(len, _period); - int startIndex = len - windowSize; - - _state.LastValidValue = double.NaN; - bool found = false; - - if (startIndex > 0) - { - for (int i = startIndex - 1; i >= 0; i--) - { - if (double.IsFinite(source.Values[i])) - { - _state.LastValidValue = source.Values[i]; - found = true; - break; - } - } - } - - if (!found) - { - for (int i = 0; i < len; i++) - { - if (double.IsFinite(source.Values[i])) - { - _state.LastValidValue = source.Values[i]; - break; - } - } - } - - _buffer.Clear(); - _state.Sum = 0; - _state.TickCount = 0; - - for (int i = startIndex; i < len; i++) - { - double val = GetValidValue(source.Values[i]); - UpdateState(val); - _state.LastInput = val; - } - - _p_state = _state; + Prime(source.Values); Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } + ///////////////////////////////////////////////////////////////////////////////////////////////// + // Mode A: Batch (Stateless) + ///////////////////////////////////////////////////////////////////////////////////////////////// + /// /// Calculates SMA for the entire series using a new instance. /// /// Input series /// SMA period /// SMA series - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var sma = new Sma(period); return sma.Update(source); @@ -218,7 +250,7 @@ public sealed class Sma : ITValuePublisher /// Output span (must be same length as source) /// SMA period (must be > 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); @@ -256,6 +288,20 @@ public sealed class Sma : ITValuePublisher CalculateScalarCore(source, output, period); } + /// + /// Runs a high-performance SIMD batch calculation on history and returns + /// a "Hot" Sma instance ready to process the next tick immediately. + /// + /// Historical time series + /// SMA Period + /// A tuple containing the full calculation results and the hot indicator instance + public static (TSeries Results, Sma Indicator) Calculate(TSeries source, int period) + { + var sma = new Sma(period); + TSeries results = sma.Update(source); + return (results, sma); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void CalculateScalarCore(ReadOnlySpan source, Span output, int period) { @@ -268,7 +314,7 @@ public sealed class Sma : ITValuePublisher double sum = 0; double lastValid = double.NaN; - + // Find first valid value to seed lastValid for (int k = 0; k < len; k++) { @@ -541,7 +587,7 @@ public sealed class Sma : ITValuePublisher /// /// Resets the SMA state. /// - public void Reset() + public override void Reset() { _buffer.Clear(); _state = default; diff --git a/lib/trends/sma/Sma.md b/lib/trends/sma/Sma.md index bccb2c51..b2d18bf7 100644 --- a/lib/trends/sma/Sma.md +++ b/lib/trends/sma/Sma.md @@ -68,12 +68,12 @@ Console.WriteLine($"IsHot: {sma.IsHot}"); // true when buffer is full // Batch calculation (TSeries API) TSeries source = ...; -TSeries results = Sma.Calculate(source, 10); +TSeries results = Sma.Batch(source, 10); // High-performance Span API (zero allocation) double[] prices = new double[10000]; double[] output = new double[10000]; -Sma.Calculate(prices.AsSpan(), output.AsSpan(), period: 10); +Sma.Batch(prices.AsSpan(), output.AsSpan(), period: 10); ``` ### Zero-Allocation Span API @@ -86,7 +86,7 @@ double[] source = new double[200000]; double[] smaOutput = new double[200000]; // Zero heap allocation during calculation -Sma.Calculate(source.AsSpan(), smaOutput.AsSpan(), period: 100); +Sma.Batch(source.AsSpan(), smaOutput.AsSpan(), period: 100); // Results are written directly to output buffer Console.WriteLine($"Last SMA: {smaOutput[^1]}"); diff --git a/lib/trends/super/Super.Quantower.Tests.cs b/lib/trends/super/Super.Quantower.Tests.cs index b8888814..6b269ee4 100644 --- a/lib/trends/super/Super.Quantower.Tests.cs +++ b/lib/trends/super/Super.Quantower.Tests.cs @@ -58,7 +58,7 @@ public class SuperIndicatorTests indicator.Initialize(); // After init, line series should exist (Up and Down) - Assert.Equal(2, indicator.LinesSeries.Length); + Assert.Equal(2, indicator.LinesSeries.Count); } [Fact] @@ -73,7 +73,7 @@ public class SuperIndicatorTests for (int i = 0; i < 20; i++) { indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i); - + // Process update for each bar to simulate history loading var args = new UpdateArgs(UpdateReason.HistoricalBar); indicator.ProcessUpdate(args); @@ -83,7 +83,7 @@ public class SuperIndicatorTests // One should be NaN, other should be value, or both NaN if cold double up = indicator.LinesSeries[0].GetValue(0); double down = indicator.LinesSeries[1].GetValue(0); - + Assert.True(double.IsFinite(up) || double.IsFinite(down)); } @@ -100,7 +100,7 @@ public class SuperIndicatorTests } indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar)); - + // Add new bar indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125); indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar)); diff --git a/lib/trends/super/Super.Quantower.cs b/lib/trends/super/Super.Quantower.cs index c45cd898..748b439b 100644 --- a/lib/trends/super/Super.Quantower.cs +++ b/lib/trends/super/Super.Quantower.cs @@ -30,10 +30,10 @@ public class SuperIndicator : Indicator, IWatchlistIndicator SeparateWindow = false; Name = "SuperTrend"; Description = "Trend-following indicator using ATR"; - + UpSeries = new(name: "SuperTrend Up", color: Color.Green, width: 2, style: LineStyle.Solid); DownSeries = new(name: "SuperTrend Down", color: Color.Red, width: 2, style: LineStyle.Solid); - + AddLineSeries(UpSeries); AddLineSeries(DownSeries); } @@ -51,7 +51,7 @@ public class SuperIndicator : Indicator, IWatchlistIndicator TBar bar = this.GetInputBar(args); TValue result = _super!.Update(bar, isNew); - + if (!_super.IsHot && !ShowColdValues) { return; diff --git a/lib/trends/super/Super.Tests.cs b/lib/trends/super/Super.Tests.cs index fd0e0a98..c11cb70c 100644 --- a/lib/trends/super/Super.Tests.cs +++ b/lib/trends/super/Super.Tests.cs @@ -142,7 +142,7 @@ public class SuperTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -154,7 +154,7 @@ public class SuperTests streamingResults.Add(super.Update(bars[i]).Value); } - var staticResults = Super.Calculate(bars, 10, 3.0); + var staticResults = Super.Batch(bars, 10, 3.0); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < staticResults.Count; i++) diff --git a/lib/trends/super/Super.cs b/lib/trends/super/Super.cs index fb77f8ae..a12d8081 100644 --- a/lib/trends/super/Super.cs +++ b/lib/trends/super/Super.cs @@ -60,6 +60,8 @@ public sealed class Super : ITValuePublisher /// public bool IsHot => _sampleCount > _period; + public int WarmupPeriod => _period + 1; + public Super(int period = 10, double multiplier = 3.0) { if (period <= 0) @@ -196,7 +198,7 @@ public sealed class Super : ITValuePublisher Last = new TValue(input.Time, superTrend); UpperBand = new TValue(input.Time, upperBand); LowerBand = new TValue(input.Time, lowerBand); - + Pub?.Invoke(Last); return Last; } @@ -218,7 +220,7 @@ public sealed class Super : ITValuePublisher return new TSeries(t, v); } - public static TSeries Calculate(TBarSeries source, int period = 10, double multiplier = 3.0) + public static TSeries Batch(TBarSeries source, int period = 10, double multiplier = 3.0) { var indicator = new Super(period, multiplier); return indicator.Update(source); diff --git a/lib/trends/super/Super.md b/lib/trends/super/Super.md index 27396d82..6069dc48 100644 --- a/lib/trends/super/Super.md +++ b/lib/trends/super/Super.md @@ -51,6 +51,14 @@ Console.WriteLine($"Lower Band: {super.LowerBand.Value}"); Console.WriteLine($"Is Bullish: {super.IsBullish}"); ``` +### Batch Calculation + +```csharp +// Calculate SuperTrend for an entire series +TBarSeries bars = ...; +TSeries result = Super.Batch(bars, period: 10, multiplier: 3.0); +``` + ### Bar Correction (isNew) ```csharp diff --git a/lib/trends/t3/T3.Tests.cs b/lib/trends/t3/T3.Tests.cs index 8886b774..02fb3ef6 100644 --- a/lib/trends/t3/T3.Tests.cs +++ b/lib/trends/t3/T3.Tests.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using Xunit; -namespace QuanTAlib; +namespace QuanTAlib.Tests; public class T3Tests { @@ -101,7 +101,7 @@ public class T3Tests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void BatchCalculate_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -114,17 +114,17 @@ public class T3Tests streamingResults.Add(t3.Update(series[i]).Value); } - var staticResults = T3.Calculate(series, 5, 0.7); + var batchResults = T3.Batch(series, 5, 0.7); - Assert.Equal(streamingResults.Count, staticResults.Count); - for (int i = 0; i < staticResults.Count; i++) + Assert.Equal(streamingResults.Count, batchResults.Count); + for (int i = 0; i < batchResults.Count; i++) { - Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9); + Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9); } } [Fact] - public void StaticCalculateSpan_Matches_Streaming() + public void BatchCalculateSpan_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -138,7 +138,7 @@ public class T3Tests } var spanResults = new double[series.Count]; - T3.Calculate(series.Values, spanResults, 5, 0.7); + T3.Batch(series.Values, spanResults, 5, 0.7); for (int i = 0; i < spanResults.Length; i++) { diff --git a/lib/trends/t3/T3.Validation.Tests.cs b/lib/trends/t3/T3.Validation.Tests.cs index 4f5406cb..4f2f6eaf 100644 --- a/lib/trends/t3/T3.Validation.Tests.cs +++ b/lib/trends/t3/T3.Validation.Tests.cs @@ -114,7 +114,7 @@ public class T3ValidationTests { // Calculate QuanTAlib T3 (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.T3.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period, vFactor); + global::QuanTAlib.T3.Batch(_testData.RawData.Span, qOutput.AsSpan(), period, vFactor); // Calculate TA-Lib T3 var retCode = TALib.Functions.T3(_testData.RawData.Span, 0..^0, talibOutput, out var outRange, period, vFactor); diff --git a/lib/trends/t3/T3.cs b/lib/trends/t3/T3.cs index 0bda877d..db0a7276 100644 --- a/lib/trends/t3/T3.cs +++ b/lib/trends/t3/T3.cs @@ -24,7 +24,7 @@ namespace QuanTAlib; /// alpha = 2 / (period + 1) /// [SkipLocalsInit] -public sealed class T3 : ITValuePublisher +public sealed class T3 : AbstractBase { private record struct State(double E1, double E2, double E3, double E4, double E5, double E6, bool IsInitialized) { @@ -67,13 +67,6 @@ public sealed class T3 : ITValuePublisher private double _lastValidValue; private double _p_lastValidValue; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; - /// /// Creates T3 with specified period and volume factor. /// @@ -99,6 +92,7 @@ public sealed class T3 : ITValuePublisher _params = new Parameters(alpha, c1, c2, c3, c4); Name = $"T3({period}, {vfactor:F2})"; + WarmupPeriod = period * 6; // T3 has 6 cascaded EMAs, so warmup is longer } /// @@ -114,14 +108,84 @@ public sealed class T3 : ITValuePublisher } /// - /// Current T3 value. + /// Creates T3 with specified source, period and volume factor. /// - public TValue Last { get; private set; } + /// Source series + /// Period for EMA calculation + /// Volume Factor (default 0.7) + public T3(TSeries source, int period, double vfactor = 0.7) : this(period, vfactor) + { + Prime(source.Values); + if (source.Count > 0) + { + Last = new TValue(source.LastTime, Last.Value); + } + source.Pub += (item) => Update(item); + } /// /// True if the T3 has been initialized (received at least one value). /// - public bool IsHot => _state.IsInitialized; + public override bool IsHot => _state.IsInitialized; + + /// + /// Initializes the indicator state using the provided history. + /// + /// Historical data + public override void Prime(ReadOnlySpan source) + { + if (source.Length == 0) return; + + // Reset state + _state = State.New(); + _p_state = State.New(); + _lastValidValue = 0; + _p_lastValidValue = 0; + + // Run the calculation on the history to update state + // We don't need the output, just the final state + int len = source.Length; + double lastValidValue = 0; + State state = _state; + + for (int i = 0; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + lastValidValue = val; + else + val = lastValidValue; + + Compute(val, _params, ref state); + } + + _state = state; + _lastValidValue = lastValidValue; + + // Calculate the initial "Last" value + // We need to re-compute the last step to get the result, or just use the state if we stored the result + // Since Compute returns the result but also updates state, we can't easily get the last result without re-running or storing it. + // However, Prime is usually followed by Update or we just need the state ready. + // If we want Last to be correct, we should probably store the last result. + // But AbstractBase.Prime doesn't strictly require Last to be set to the very last value of source, + // though it's good practice. + // Let's re-run the last value computation to set Last correctly. + if (len > 0) + { + // We need to be careful not to double-apply the last update if we just loop. + // Actually, the loop above updated the state to include the last value. + // So the state corresponds to "after processing source". + // To get the output value corresponding to the last input, we can calculate it from the state. + // But T3 formula uses the *updated* EMAs. + // T3 = c1*e6 + c2*e5 + c3*e4 + c4*e3 + // The state has the updated EMAs. + double result = _params.C1 * _state.E6 + _params.C2 * _state.E5 + _params.C3 * _state.E4 + _params.C4 * _state.E3; + Last = new TValue(DateTime.MinValue, result); + } + + _p_state = _state; + _p_lastValidValue = _lastValidValue; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) @@ -135,7 +199,7 @@ public sealed class T3 : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -151,11 +215,11 @@ public sealed class T3 : ITValuePublisher double val = GetValidValue(input.Value); val = Compute(val, _params, ref _state); Last = new TValue(input.Time, val); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -227,7 +291,7 @@ public sealed class T3 : ITValuePublisher /// /// Calculates T3 for the entire series using a new instance. /// - public static TSeries Calculate(TSeries source, int period, double vfactor = 0.7) + public static TSeries Batch(TSeries source, int period, double vfactor = 0.7) { var t3 = new T3(period, vfactor); return t3.Update(source); @@ -237,7 +301,7 @@ public sealed class T3 : ITValuePublisher /// Calculates T3 in-place using period, writing results to pre-allocated output span. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period, double vfactor = 0.7) + public static void Batch(ReadOnlySpan source, Span output, int period, double vfactor = 0.7) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); @@ -264,7 +328,7 @@ public sealed class T3 : ITValuePublisher /// /// Resets the T3 state. /// - public void Reset() + public override void Reset() { _state = State.New(); _p_state = _state; diff --git a/lib/trends/t3/T3.md b/lib/trends/t3/T3.md index e1dfe784..896c9805 100644 --- a/lib/trends/t3/T3.md +++ b/lib/trends/t3/T3.md @@ -45,10 +45,10 @@ Where: ```csharp // Calculate T3 with period 10 and default volume factor 0.7 -var t3 = T3.Calculate(sourceSeries, 10); +var t3 = T3.Batch(sourceSeries, 10); // Calculate T3 with period 10 and volume factor 0.618 -var t3_custom = T3.Calculate(sourceSeries, 10, 0.618); +var t3_custom = T3.Batch(sourceSeries, 10, 0.618); Console.WriteLine($"T3 Value: {t3.Last.Value}"); ``` diff --git a/lib/trends/tema/Tema.Quantower.Tests.cs b/lib/trends/tema/Tema.Quantower.Tests.cs index d6fb1580..d29e812c 100644 --- a/lib/trends/tema/Tema.Quantower.Tests.cs +++ b/lib/trends/tema/Tema.Quantower.Tests.cs @@ -116,7 +116,7 @@ public class TemaIndicatorTests { var indicator = new TemaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(TemaIndicator), method.DeclaringType); diff --git a/lib/trends/tema/Tema.Tests.cs b/lib/trends/tema/Tema.Tests.cs index 41086453..c52bfdb3 100644 --- a/lib/trends/tema/Tema.Tests.cs +++ b/lib/trends/tema/Tema.Tests.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using Xunit; -namespace QuanTAlib; +namespace QuanTAlib.Tests; public class TemaTests { @@ -101,7 +101,7 @@ public class TemaTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void BatchCalculate_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -114,17 +114,17 @@ public class TemaTests streamingResults.Add(tema.Update(series[i]).Value); } - var staticResults = Tema.Calculate(series, 10); + var batchResults = Tema.Batch(series, 10); - Assert.Equal(streamingResults.Count, staticResults.Count); - for (int i = 0; i < staticResults.Count; i++) + Assert.Equal(streamingResults.Count, batchResults.Count); + for (int i = 0; i < batchResults.Count; i++) { - Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9); + Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9); } } [Fact] - public void StaticCalculateSpan_Matches_Streaming() + public void BatchCalculateSpan_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -138,7 +138,7 @@ public class TemaTests } var spanResults = new double[series.Count]; - Tema.Calculate(series.Values, spanResults, 10); + Tema.Batch(series.Values, spanResults, 10); for (int i = 0; i < spanResults.Length; i++) { diff --git a/lib/trends/tema/Tema.Validation.Tests.cs b/lib/trends/tema/Tema.Validation.Tests.cs index 9a138e28..8b6a8c7c 100644 --- a/lib/trends/tema/Tema.Validation.Tests.cs +++ b/lib/trends/tema/Tema.Validation.Tests.cs @@ -112,7 +112,7 @@ public class TemaValidationTests { // Calculate QuanTAlib TEMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Tema.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period); + global::QuanTAlib.Tema.Batch(_testData.RawData.Span, qOutput.AsSpan(), period); // Calculate TA-Lib TEMA var retCode = TALib.Functions.Tema(_testData.RawData.Span, 0..^0, talibOutput, out var outRange, period); diff --git a/lib/trends/tema/Tema.cs b/lib/trends/tema/Tema.cs index 682ee59c..50e5ee47 100644 --- a/lib/trends/tema/Tema.cs +++ b/lib/trends/tema/Tema.cs @@ -24,7 +24,7 @@ namespace QuanTAlib; /// which is faster than the standard EMA convergence (3/alpha steps). /// [SkipLocalsInit] -public sealed class Tema : ITValuePublisher +public sealed class Tema : AbstractBase { private record struct EmaState(double Ema, double E, bool IsHot, bool IsCompensated) { @@ -33,21 +33,18 @@ public sealed class Tema : ITValuePublisher private readonly double _alpha; private readonly double _decay; - + private EmaState _state1 = EmaState.New(); private EmaState _state2 = EmaState.New(); private EmaState _state3 = EmaState.New(); private EmaState _p_state1 = EmaState.New(); private EmaState _p_state2 = EmaState.New(); private EmaState _p_state3 = EmaState.New(); - + private double _lastValidValue; private double _p_lastValidValue; - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _state3.E <= 0.09; - public event Action? Pub; + public override bool IsHot => _state3.E <= 0.09; public Tema(int period) { @@ -56,6 +53,7 @@ public sealed class Tema : ITValuePublisher _alpha = 2.0 / (period + 1); _decay = 1.0 - _alpha; Name = $"Tema({period})"; + WarmupPeriod = period * 3; } public Tema(ITValuePublisher source, int period) : this(period) @@ -63,6 +61,16 @@ public sealed class Tema : ITValuePublisher source.Pub += (item) => Update(item); } + public Tema(TSeries source, int period) : this(period) + { + Prime(source.Values); + if (source.Count > 0) + { + Last = new TValue(source.LastTime, Last.Value); + } + source.Pub += (item) => Update(item); + } + public Tema(double alpha) { if (alpha <= 0 || alpha >= 1) throw new ArgumentException("Alpha must be strictly between 0 and 1", nameof(alpha)); @@ -70,10 +78,86 @@ public sealed class Tema : ITValuePublisher _alpha = alpha; _decay = 1.0 - alpha; Name = $"Tema(α={alpha:F4})"; + WarmupPeriod = (int)(3 * (2.0 / alpha - 1.0)); + } + + /// + /// Initializes the indicator state using the provided history. + /// + /// Historical data + public override void Prime(ReadOnlySpan source) + { + if (source.Length == 0) return; + + // Reset state + _state1 = EmaState.New(); + _state2 = EmaState.New(); + _state3 = EmaState.New(); + _p_state1 = EmaState.New(); + _p_state2 = EmaState.New(); + _p_state3 = EmaState.New(); + _lastValidValue = 0; + _p_lastValidValue = 0; + + // Run the calculation on the history to update state + // We don't need the output, just the final state + int len = source.Length; + double lastValid = 0; + EmaState s1 = _state1; + EmaState s2 = _state2; + EmaState s3 = _state3; + double alpha = _alpha; + double decay = _decay; + + for (int i = 0; i < len; i++) + { + double val = source[i]; + if (double.IsFinite(val)) + lastValid = val; + else + val = lastValid; + + double e1 = Compute(val, alpha, decay, ref s1); + double e2 = Compute(e1, alpha, decay, ref s2); + Compute(e2, alpha, decay, ref s3); + } + + _state1 = s1; + _state2 = s2; + _state3 = s3; + _lastValidValue = lastValid; + + // Calculate the initial "Last" value + // We need to re-compute the last step to get the result + // But Compute updates state, so we can't just call it again without side effects if we pass ref state. + // However, we can calculate the result from the current state. + // TEMA = 3 * EMA1 - 3 * EMA2 + EMA3 + // The state contains the updated EMA values (Ema field). + // But wait, Compute returns the *compensated* value. + // The state.Ema is the raw EMA value. + // We need to apply compensation logic to get the correct E1, E2, E3. + + double GetCompensated(EmaState s) + { + if (s.IsCompensated) return s.Ema; + return s.Ema / (1.0 - s.E); + } + + double e1_final = GetCompensated(_state1); + double e2_final = GetCompensated(_state2); + double e3_final = GetCompensated(_state3); + double result = 3 * e1_final - 3 * e2_final + e3_final; + + Last = new TValue(DateTime.MinValue, result); + + _p_state1 = _state1; + _p_state2 = _state2; + _p_state3 = _state3; + _p_lastValidValue = _lastValidValue; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -107,11 +191,11 @@ public sealed class Tema : ITValuePublisher double result = 3 * e1 - 3 * e2 + e3; Last = new TValue(input.Time, result); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -126,7 +210,7 @@ public sealed class Tema : ITValuePublisher source.Times.CopyTo(tSpan); var sourceValues = source.Values; - + // Use current state EmaState s1 = _state1; EmaState s2 = _state2; @@ -195,28 +279,28 @@ public sealed class Tema : ITValuePublisher return result; } - public static TSeries Calculate(TSeries source, int period) + public static TSeries Batch(TSeries source, int period) { var tema = new Tema(period); return tema.Update(source); } - public static TSeries Calculate(TSeries source, double alpha) + public static TSeries Batch(TSeries source, double alpha) { var tema = new Tema(alpha); return tema.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); double alpha = 2.0 / (period + 1); - Calculate(source, output, alpha); + Batch(source, output, alpha); } - public static void Calculate(ReadOnlySpan source, Span output, double alpha) + public static void Batch(ReadOnlySpan source, Span output, double alpha) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); @@ -319,7 +403,7 @@ public sealed class Tema : ITValuePublisher } } - public void Reset() + public override void Reset() { _state1 = EmaState.New(); _state2 = EmaState.New(); diff --git a/lib/trends/tema/Tema.md b/lib/trends/tema/Tema.md index 2a12e6c5..ed6d3c16 100644 --- a/lib/trends/tema/Tema.md +++ b/lib/trends/tema/Tema.md @@ -61,12 +61,12 @@ Console.WriteLine($"Current TEMA: {result.Value}"); // Batch calculation (TSeries API) TSeries source = ...; -TSeries results = Tema.Calculate(source, 14); +TSeries results = Tema.Batch(source, 14); // High-performance Span API (zero allocation) double[] prices = new double[10000]; double[] output = new double[10000]; -Tema.Calculate(prices.AsSpan(), output.AsSpan(), period: 14); +Tema.Batch(prices.AsSpan(), output.AsSpan(), period: 14); ``` ### Zero-Allocation Span API @@ -79,7 +79,7 @@ double[] source = new double[200000]; double[] temaOutput = new double[200000]; // Zero heap allocation during calculation -Tema.Calculate(source.AsSpan(), temaOutput.AsSpan(), period: 50); +Tema.Batch(source.AsSpan(), temaOutput.AsSpan(), period: 50); ``` ### Eventing and Reactive Support diff --git a/lib/trends/trima/Trima.Quantower.Tests.cs b/lib/trends/trima/Trima.Quantower.Tests.cs index 9629efb7..1c271b20 100644 --- a/lib/trends/trima/Trima.Quantower.Tests.cs +++ b/lib/trends/trima/Trima.Quantower.Tests.cs @@ -116,7 +116,7 @@ public class TrimaIndicatorTests { var indicator = new TrimaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(TrimaIndicator), method.DeclaringType); diff --git a/lib/trends/trima/Trima.Tests.cs b/lib/trends/trima/Trima.Tests.cs index d0876e9b..bb6c34d8 100644 --- a/lib/trends/trima/Trima.Tests.cs +++ b/lib/trends/trima/Trima.Tests.cs @@ -101,7 +101,7 @@ public class TrimaTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void BatchCalculate_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -114,17 +114,17 @@ public class TrimaTests streamingResults.Add(trima.Update(series[i]).Value); } - var staticResults = Trima.Calculate(series, 10); + var batchResults = Trima.Batch(series, 10); - Assert.Equal(streamingResults.Count, staticResults.Count); - for (int i = 0; i < staticResults.Count; i++) + Assert.Equal(streamingResults.Count, batchResults.Count); + for (int i = 0; i < batchResults.Count; i++) { - Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9); + Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9); } } [Fact] - public void StaticCalculateSpan_Matches_Streaming() + public void BatchCalculateSpan_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -138,7 +138,7 @@ public class TrimaTests } var spanResults = new double[series.Count]; - Trima.Calculate(series.Values, spanResults, 10); + Trima.Batch(series.Values, spanResults, 10); for (int i = 0; i < spanResults.Length; i++) { diff --git a/lib/trends/trima/Trima.Validation.Tests.cs b/lib/trends/trima/Trima.Validation.Tests.cs index 31e63dbf..805f35ee 100644 --- a/lib/trends/trima/Trima.Validation.Tests.cs +++ b/lib/trends/trima/Trima.Validation.Tests.cs @@ -126,7 +126,7 @@ public class TrimaValidationTests { // Calculate QuanTAlib TRIMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Trima.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period); + global::QuanTAlib.Trima.Batch(_testData.RawData.Span, qOutput.AsSpan(), period); // Calculate TA-Lib TRIMA var retCode = TALib.Functions.Trima(_testData.RawData.Span, 0..^0, talibOutput, out var outRange, period); diff --git a/lib/trends/trima/Trima.cs b/lib/trends/trima/Trima.cs index 79dfa1f7..b3b4c05a 100644 --- a/lib/trends/trima/Trima.cs +++ b/lib/trends/trima/Trima.cs @@ -1,4 +1,6 @@ +using System; using System.Buffers; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -20,44 +22,28 @@ namespace QuanTAlib; /// Uses two SMA instances, each with O(1) update complexity. /// /// IsHot: -/// Becomes true when the buffer is full (period samples processed). +/// Becomes true when both internal SMAs are hot. /// [SkipLocalsInit] -public sealed class Trima : ITValuePublisher +public sealed class Trima : AbstractBase { private readonly int _period; - private readonly int _p1; - private readonly int _p2; - private readonly RingBuffer _buffer1; - private readonly RingBuffer _buffer2; - - private record struct State( - double Sum1, double LastInput1, double LastValidValue1, int TickCount1, double NextRemoved1, - double Sum2, double LastInput2, int TickCount2, double NextRemoved2, - int SampleCount - ); - private State _state; - private State _p_state; - - private const int ResyncInterval = 1000; - - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _state.SampleCount >= _period; - public event Action? Pub; + private readonly Sma _sma1; + private readonly Sma _sma2; public Trima(int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); _period = period; - _p1 = period / 2 + 1; - _p2 = (period + 1) / 2; - - _buffer1 = new RingBuffer(_p1); - _buffer2 = new RingBuffer(_p2); - + int p1 = period / 2 + 1; + int p2 = (period + 1) / 2; + + _sma1 = new Sma(p1); + _sma2 = new Sma(p2); + Name = $"Trima({period})"; + WarmupPeriod = p1 + p2 - 1; } public Trima(ITValuePublisher source, int period) : this(period) @@ -65,129 +51,78 @@ public sealed class Trima : ITValuePublisher source.Pub += (item) => Update(item); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private double GetValidValue(double input) - { - if (double.IsFinite(input)) - { - _state.LastValidValue1 = input; - return input; - } - return _state.LastValidValue1; - } + public override bool IsHot => _sma1.IsHot && _sma2.IsHot; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { - if (isNew) - { - _p_state = _state; - _state.SampleCount++; - } - else - { - _state = _p_state; - } + TValue v1 = _sma1.Update(input, isNew); + TValue v2 = _sma2.Update(v1, isNew); - // SMA 1 - double val1 = GetValidValue(input.Value); - - if (isNew) - { - double removed1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0; - _state.Sum1 = _state.Sum1 - removed1 + val1; - _buffer1.Add(val1); - - // Store NextRemoved1 for next step - _state.NextRemoved1 = _buffer1.Count == _buffer1.Capacity ? _buffer1.Oldest : 0.0; - - _state.TickCount1++; - if (_buffer1.IsFull && _state.TickCount1 >= ResyncInterval) - { - _state.TickCount1 = 0; - _state.Sum1 = _buffer1.Sum(); - } - } - else - { - // Use NextRemoved1 from _p_state - double removed1 = _p_state.NextRemoved1; - _state.Sum1 = _p_state.Sum1 - removed1 + val1; - _buffer1.UpdateNewest(val1); - } - - _state.LastInput1 = val1; - double sma1Result = _state.Sum1 / _buffer1.Count; - - // SMA 2 - if (isNew) - { - double removed2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0; - _state.Sum2 = _state.Sum2 - removed2 + sma1Result; - _buffer2.Add(sma1Result); - - // Store NextRemoved2 for next step - _state.NextRemoved2 = _buffer2.Count == _buffer2.Capacity ? _buffer2.Oldest : 0.0; - - _state.TickCount2++; - if (_buffer2.IsFull && _state.TickCount2 >= ResyncInterval) - { - _state.TickCount2 = 0; - _state.Sum2 = _buffer2.Sum(); - } - } - else - { - // Use NextRemoved2 from _p_state - double removed2 = _p_state.NextRemoved2; - _state.Sum2 = _p_state.Sum2 - removed2 + sma1Result; - _buffer2.UpdateNewest(sma1Result); - } - - _state.LastInput2 = sma1Result; - - Last = new TValue(input.Time, _state.Sum2 / _buffer2.Count); - Pub?.Invoke(Last); + Last = v2; + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; int len = source.Count; - List t = new(len); - List v = new(len); + var t = new List(len); + var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - - Calculate(source.Values, vSpan, _period); + + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); - // Restore state - int lookback = _p1 + _p2 - 2; - int startIndex = Math.Max(0, len - lookback); - Reset(); - - for (int i = startIndex; i < len; i++) - { - Update(new TValue(source.Times[i], source.Values[i]), isNew: true); - } + Prime(source.Values); Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public override void Prime(ReadOnlySpan source) + { + _sma1.Reset(); + _sma2.Reset(); + + _sma1.Prime(source); + + // Calculate intermediate SMA series to prime the second SMA + int p1 = _period / 2 + 1; + double[] tempArray = ArrayPool.Shared.Rent(source.Length); + Span tempSpan = tempArray.AsSpan(0, source.Length); + + try + { + Sma.Batch(source, tempSpan, p1); + _sma2.Prime(tempSpan); + } + finally + { + ArrayPool.Shared.Return(tempArray); + } + } + + public override void Reset() + { + _sma1.Reset(); + _sma2.Reset(); + Last = default; + } + + public static TSeries Batch(TSeries source, int period) { var trima = new Trima(period); return trima.Update(source); } - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); @@ -202,21 +137,12 @@ public sealed class Trima : ITValuePublisher try { - Sma.Calculate(source, tempSpan, p1); - Sma.Calculate(tempSpan, output, p2); + Sma.Batch(source, tempSpan, p1); + Sma.Batch(tempSpan, output, p2); } finally { ArrayPool.Shared.Return(tempArray); } } - - public void Reset() - { - _buffer1.Clear(); - _buffer2.Clear(); - _state = default; - _p_state = default; - Last = default; - } } diff --git a/lib/trends/trima/Trima.md b/lib/trends/trima/Trima.md index 09b61af2..6e49afd2 100644 --- a/lib/trends/trima/Trima.md +++ b/lib/trends/trima/Trima.md @@ -43,6 +43,29 @@ TRIMA(source, p) = SMA(SMA(source, (p+1)/2), (p+1)/2) ## C# Implementation +### Standard Usage + +```csharp +using QuanTAlib; + +// Create TRIMA with period 14 +var trima = new Trima(14); + +// Update with new value +var result = trima.Update(new TValue(DateTime.UtcNow, 100.0)); +Console.WriteLine($"TRIMA: {result.Value}"); +``` + +### Static API (High Performance) + +```csharp +// Calculate TRIMA for an entire array +double[] prices = { ... }; +double[] results = new double[prices.Length]; + +Trima.Batch(prices, results, 14); +``` + ### Eventing and Reactive Support This indicator implements the `ITValuePublisher` interface, enabling event-driven and reactive workflows. diff --git a/lib/trends/vidya/Vidya.Tests.cs b/lib/trends/vidya/Vidya.Tests.cs index 75b39ee1..4cc92f87 100644 --- a/lib/trends/vidya/Vidya.Tests.cs +++ b/lib/trends/vidya/Vidya.Tests.cs @@ -100,7 +100,7 @@ public class VidyaTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void BatchCalculate_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -113,17 +113,17 @@ public class VidyaTests streamingResults.Add(vidya.Update(series[i]).Value); } - var staticResults = Vidya.Calculate(series, 10); + var batchResults = Vidya.Batch(series, 10); - Assert.Equal(streamingResults.Count, staticResults.Count); - for (int i = 0; i < staticResults.Count; i++) + Assert.Equal(streamingResults.Count, batchResults.Count); + for (int i = 0; i < batchResults.Count; i++) { - Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9); + Assert.Equal(streamingResults[i], batchResults.Values[i], 1e-9); } } [Fact] - public void StaticCalculateSpan_Matches_Streaming() + public void BatchCalculateSpan_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -137,7 +137,7 @@ public class VidyaTests } var spanResults = new double[series.Count]; - Vidya.Calculate(series.Values, spanResults, 10); + Vidya.Batch(series.Values, spanResults, 10); for (int i = 0; i < spanResults.Length; i++) { diff --git a/lib/trends/vidya/Vidya.Validation.Tests.cs b/lib/trends/vidya/Vidya.Validation.Tests.cs index a45f6862..e7882eef 100644 --- a/lib/trends/vidya/Vidya.Validation.Tests.cs +++ b/lib/trends/vidya/Vidya.Validation.Tests.cs @@ -49,6 +49,22 @@ public class VidyaValidationTests _output.WriteLine("VIDYA validated successfully against reference implementation"); } + [Fact] + public void ValidateBatchAgainstReference() + { + var period = 14; + + // QuanTAlib Batch + var qResults = Vidya.Batch(_testData.Data, period); + + // Reference Implementation + var refResults = CalculateVidyaReference(_testData.Data, period); + + // Compare + ValidationHelper.VerifyData(qResults, refResults, x => x); + + _output.WriteLine("VIDYA Batch validated successfully against reference implementation"); + } private static List CalculateVidyaReference(TSeries data, int period) { diff --git a/lib/trends/vidya/Vidya.cs b/lib/trends/vidya/Vidya.cs index 17a0c40f..7023ab37 100644 --- a/lib/trends/vidya/Vidya.cs +++ b/lib/trends/vidya/Vidya.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -24,8 +26,9 @@ namespace QuanTAlib; /// - Reacts quickly in trending markets (high volatility) /// [SkipLocalsInit] -public sealed class Vidya : ITValuePublisher +public sealed class Vidya : AbstractBase { + private readonly int _period; private readonly double _alpha; private readonly RingBuffer _ups; private readonly RingBuffer _downs; @@ -38,42 +41,28 @@ public sealed class Vidya : ITValuePublisher private State _state; private State _p_state; - /// - /// Display name for the indicator. - /// - public string Name { get; } - - public event Action? Pub; - - public TValue Last { get; private set; } - - /// - /// Creates VIDYA with specified period. - /// - /// Period for calculation (must be > 0) public Vidya(int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); + _period = period; _alpha = 2.0 / (period + 1); _ups = new RingBuffer(period); _downs = new RingBuffer(period); Name = $"Vidya({period})"; + WarmupPeriod = period; } - /// - /// Creates VIDYA with specified source and period. - /// - /// Source to subscribe to - /// Period for calculation public Vidya(ITValuePublisher source, int period) : this(period) { source.Pub += (item) => Update(item); } + public override bool IsHot => _state.BarCount >= _period; + [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -84,7 +73,8 @@ public sealed class Vidya : ITValuePublisher _state = _p_state; } - _state.BarCount++; + if (isNew) _state.BarCount++; + if (_state.IsInitialized) { _state.PrevClose = _state.CurrentClose; @@ -94,10 +84,8 @@ public sealed class Vidya : ITValuePublisher double price = input.Value; if (!double.IsFinite(price)) { - // Handle NaN/Infinity by using the last known valid values - // If not initialized, we can't do much, just return input if (!_state.IsInitialized) return input; - price = _state.CurrentClose; // Use last valid close + price = _state.CurrentClose; } if (_state.BarCount <= 1) @@ -110,7 +98,7 @@ public sealed class Vidya : ITValuePublisher _ups.Add(0, isNew); _downs.Add(0, isNew); Last = new TValue(input.Time, _state.CurrentVidya); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } @@ -136,11 +124,11 @@ public sealed class Vidya : ITValuePublisher _state.CurrentClose = price; Last = new TValue(input.Time, _state.CurrentVidya); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; @@ -152,31 +140,101 @@ public sealed class Vidya : ITValuePublisher var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - var sourceValues = source.Values; - var sourceTimes = source.Times; - sourceTimes.CopyTo(tSpan); + Batch(source.Values, vSpan, _period); + source.Times.CopyTo(tSpan); - Reset(); - for (int i = 0; i < len; i++) - { - var val = Update(new TValue(sourceTimes[i], sourceValues[i]), true); - vSpan[i] = val.Value; - } + Prime(source.Values); + Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public override void Prime(ReadOnlySpan source) + { + if (source.Length == 0) return; + + // Reset state + Reset(); + + // Process all data to build up state + // For recursive indicators like VIDYA, we generally need to process from the start + // or at least a significant warmup period. + // Given we don't know the "correct" previous VIDYA without processing, + // we process the whole provided history. + + double prevClose = source[0]; + double lastVidya = source[0]; + + // Initialize state + _state.PrevClose = prevClose; + _state.LastVidya = lastVidya; + _state.CurrentClose = prevClose; + _state.CurrentVidya = lastVidya; + _state.IsInitialized = true; + _state.BarCount = 1; + _ups.Add(0); + _downs.Add(0); + + for (int i = 1; i < source.Length; i++) + { + double price = source[i]; + if (!double.IsFinite(price)) price = prevClose; + + double change = price - prevClose; + double up = change > 0 ? change : 0; + double down = change < 0 ? -change : 0; + + _ups.Add(up); + _downs.Add(down); + _state.BarCount++; + + double sumUp = _ups.Sum; + double sumDown = _downs.Sum; + double sum = sumUp + sumDown; + + double vi = 0; + if (sum > double.Epsilon) + { + vi = Math.Abs(sumUp - sumDown) / sum; + } + + double dynamicAlpha = _alpha * vi; + double currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * lastVidya; + + _state.CurrentVidya = currentVidya; + _state.CurrentClose = price; + + prevClose = price; + lastVidya = currentVidya; + } + + _state.PrevClose = prevClose; + _state.LastVidya = lastVidya; + + // Set Last + // Note: Time is not available in Span, so we use MinValue. + // It will be updated on next Update. + Last = new TValue(DateTime.MinValue, _state.CurrentVidya); + _p_state = _state; + } + + public override void Reset() + { + _ups.Clear(); + _downs.Clear(); + _state = default; + _p_state = default; + Last = default; + } + + public static TSeries Batch(TSeries source, int period) { var vidya = new Vidya(period); return vidya.Update(source); } - /// - /// Calculates VIDYA for the entire series. - /// - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); @@ -187,63 +245,71 @@ public sealed class Vidya : ITValuePublisher double alpha = 2.0 / (period + 1); - double[] ups = new double[period]; - double[] downs = new double[period]; - int head = 0; - double sumUp = 0; - double sumDown = 0; + // Use arrays for buffers to avoid heap allocations if possible, + // but period is dynamic. + // We can use ArrayPool or just new double[period] if period is small. + // For simplicity and safety with large periods, let's use ArrayPool. - double prevClose = source[0]; - double lastVidya = source[0]; + double[] ups = System.Buffers.ArrayPool.Shared.Rent(period); + double[] downs = System.Buffers.ArrayPool.Shared.Rent(period); + Array.Clear(ups, 0, period); + Array.Clear(downs, 0, period); - output[0] = source[0]; - - for (int i = 1; i < source.Length; i++) + try { - double price = source[i]; - if (!double.IsFinite(price)) + int head = 0; + double sumUp = 0; + double sumDown = 0; + + double prevClose = source[0]; + double lastVidya = source[0]; + + output[0] = source[0]; + + for (int i = 1; i < source.Length; i++) { - price = prevClose; + double price = source[i]; + if (!double.IsFinite(price)) + { + price = prevClose; + } + + double change = price - prevClose; + double up = change > 0 ? change : 0; + double down = change < 0 ? -change : 0; + + sumUp -= ups[head]; + sumDown -= downs[head]; + + ups[head] = up; + downs[head] = down; + + sumUp += up; + sumDown += down; + + head = (head + 1); + if (head >= period) head = 0; + + double sum = sumUp + sumDown; + double vi = 0; + if (sum > double.Epsilon) + { + vi = Math.Abs(sumUp - sumDown) / sum; + } + + double dynamicAlpha = alpha * vi; + double currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * lastVidya; + + output[i] = currentVidya; + + prevClose = price; + lastVidya = currentVidya; } - - double change = price - prevClose; - double up = change > 0 ? change : 0; - double down = change < 0 ? -change : 0; - - sumUp -= ups[head]; - sumDown -= downs[head]; - - ups[head] = up; - downs[head] = down; - - sumUp += up; - sumDown += down; - - head = (head + 1) % period; - - double sum = sumUp + sumDown; - double vi = 0; - if (sum > double.Epsilon) - { - vi = Math.Abs(sumUp - sumDown) / sum; - } - - double dynamicAlpha = alpha * vi; - double currentVidya = dynamicAlpha * price + (1.0 - dynamicAlpha) * lastVidya; - - output[i] = currentVidya; - - prevClose = price; - lastVidya = currentVidya; + } + finally + { + System.Buffers.ArrayPool.Shared.Return(ups); + System.Buffers.ArrayPool.Shared.Return(downs); } } - - public void Reset() - { - _ups.Clear(); - _downs.Clear(); - _state = default; - _p_state = default; - Last = default; - } } diff --git a/lib/trends/vidya/Vidya.md b/lib/trends/vidya/Vidya.md index 26aabe7f..b856ec8b 100644 --- a/lib/trends/vidya/Vidya.md +++ b/lib/trends/vidya/Vidya.md @@ -71,7 +71,7 @@ Console.WriteLine($"VIDYA: {result.Value}"); double[] prices = { ... }; double[] results = new double[prices.Length]; -Vidya.Calculate(prices, results, 14); +Vidya.Batch(prices, results, 14); ``` ### Bar Correction (Streaming) diff --git a/lib/trends/wma/Wma.Quantower.Tests.cs b/lib/trends/wma/Wma.Quantower.Tests.cs index f82ded07..b6b52fcf 100644 --- a/lib/trends/wma/Wma.Quantower.Tests.cs +++ b/lib/trends/wma/Wma.Quantower.Tests.cs @@ -116,7 +116,7 @@ public class WmaIndicatorTests { var indicator = new WmaIndicator(); indicator.Initialize(); - + var method = indicator.GetType().GetMethod("OnPaintChart"); Assert.NotNull(method); Assert.Equal(typeof(WmaIndicator), method.DeclaringType); diff --git a/lib/trends/wma/Wma.Tests.cs b/lib/trends/wma/Wma.Tests.cs index ac0fb54c..6222dbf1 100644 --- a/lib/trends/wma/Wma.Tests.cs +++ b/lib/trends/wma/Wma.Tests.cs @@ -101,7 +101,7 @@ public class WmaTests } [Fact] - public void StaticCalculate_Matches_Streaming() + public void StaticBatch_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -114,7 +114,7 @@ public class WmaTests streamingResults.Add(wma.Update(series[i]).Value); } - var staticResults = Wma.Calculate(series, 10); + var staticResults = Wma.Batch(series, 10); Assert.Equal(streamingResults.Count, staticResults.Count); for (int i = 0; i < staticResults.Count; i++) @@ -124,7 +124,7 @@ public class WmaTests } [Fact] - public void StaticCalculateSpan_Matches_Streaming() + public void StaticBatchSpan_Matches_Streaming() { var gbm = new GBM(); var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); @@ -138,7 +138,7 @@ public class WmaTests } var spanResults = new double[series.Count]; - Wma.Calculate(series.Values, spanResults, 10); + Wma.Batch(series.Values, spanResults, 10); for (int i = 0; i < spanResults.Length; i++) { diff --git a/lib/trends/wma/Wma.Validation.Tests.cs b/lib/trends/wma/Wma.Validation.Tests.cs index 0a0b1313..6d43a810 100644 --- a/lib/trends/wma/Wma.Validation.Tests.cs +++ b/lib/trends/wma/Wma.Validation.Tests.cs @@ -75,7 +75,7 @@ public class WmaValidationTests { // Calculate QuanTAlib WMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Wma.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period); + global::QuanTAlib.Wma.Batch(_testData.RawData.Span, qOutput.AsSpan(), period); // Calculate Skender WMA var sResult = _testData.SkenderQuotes.GetWma(period).ToList(); @@ -154,7 +154,7 @@ public class WmaValidationTests { // Calculate QuanTAlib WMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Wma.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period); + global::QuanTAlib.Wma.Batch(_testData.RawData.Span, qOutput.AsSpan(), period); // Calculate TA-Lib WMA var retCode = TALib.Functions.Wma(_testData.RawData.Span, 0..^0, talibOutput, out var outRange, period); @@ -235,7 +235,7 @@ public class WmaValidationTests { // Calculate QuanTAlib WMA (Span API) double[] qOutput = new double[_testData.RawData.Length]; - global::QuanTAlib.Wma.Calculate(_testData.RawData.Span, qOutput.AsSpan(), period); + global::QuanTAlib.Wma.Batch(_testData.RawData.Span, qOutput.AsSpan(), period); // Calculate Tulip WMA var wmaIndicator = Tulip.Indicators.wma; diff --git a/lib/trends/wma/Wma.cs b/lib/trends/wma/Wma.cs index 2d9c1d6b..ac236608 100644 --- a/lib/trends/wma/Wma.cs +++ b/lib/trends/wma/Wma.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -26,7 +27,7 @@ namespace QuanTAlib; /// Becomes true when the buffer is full (period samples processed). /// [SkipLocalsInit] -public sealed class Wma : ITValuePublisher +public sealed class Wma : AbstractBase { private readonly int _period; private readonly double _divisor; @@ -38,11 +39,6 @@ public sealed class Wma : ITValuePublisher private const int ResyncInterval = 1000; - public string Name { get; } - public TValue Last { get; private set; } - public bool IsHot => _buffer.IsFull; - public event Action? Pub; - public Wma(int period) { if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period)); @@ -51,6 +47,7 @@ public sealed class Wma : ITValuePublisher _divisor = (double)period * (period + 1) * 0.5; _buffer = new RingBuffer(period); Name = $"Wma({period})"; + WarmupPeriod = period; } public Wma(ITValuePublisher source, int period) : this(period) @@ -58,6 +55,8 @@ public sealed class Wma : ITValuePublisher source.Pub += (item) => Update(item); } + public override bool IsHot => _buffer.IsFull; + [MethodImpl(MethodImplOptions.AggressiveInlining)] private double GetValidValue(double input) { @@ -107,7 +106,7 @@ public sealed class Wma : ITValuePublisher } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TValue Update(TValue input, bool isNew = true) + public override TValue Update(TValue input, bool isNew = true) { if (isNew) { @@ -131,73 +130,91 @@ public sealed class Wma : ITValuePublisher double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * 0.5; Last = new TValue(input.Time, _state.WSum / currentDivisor); - Pub?.Invoke(Last); + PubEvent(Last); return Last; } - public TSeries Update(TSeries source) + public override TSeries Update(TSeries source) { if (source.Count == 0) return []; int len = source.Count; - List t = new(len); - List v = new(len); + var t = new List(len); + var v = new List(len); CollectionsMarshal.SetCount(t, len); CollectionsMarshal.SetCount(v, len); var tSpan = CollectionsMarshal.AsSpan(t); var vSpan = CollectionsMarshal.AsSpan(v); - - Calculate(source.Values, vSpan, _period); + + Batch(source.Values, vSpan, _period); source.Times.CopyTo(tSpan); - // Restore state - int windowSize = Math.Min(len, _period); - int startIndex = len - windowSize; - - if (startIndex > 0) - { - _state.LastValidValue = 0; - for (int i = startIndex - 1; i >= 0; i--) - { - if (double.IsFinite(source.Values[i])) - { - _state.LastValidValue = source.Values[i]; - break; - } - } - } - else - { - _state.LastValidValue = 0; - } - - _buffer.Clear(); - _state.Sum = 0; - _state.WSum = 0; - _state.TickCount = 0; - - for (int i = startIndex; i < len; i++) - { - double val = GetValidValue(source.Values[i]); - UpdateState(val); - _state.LastInput = val; - } - - _p_state = _state; + Prime(source.Values); Last = new TValue(tSpan[len - 1], vSpan[len - 1]); return new TSeries(t, v); } - public static TSeries Calculate(TSeries source, int period) + public override void Prime(ReadOnlySpan source) + { + if (source.Length == 0) return; + + int len = source.Length; + int windowSize = Math.Min(len, _period); + int startIndex = len - windowSize; + + // Seed LastValidValue + _state.LastValidValue = 0; + if (startIndex > 0) + { + for (int i = startIndex - 1; i >= 0; i--) + { + if (double.IsFinite(source[i])) + { + _state.LastValidValue = source[i]; + break; + } + } + } + + // Reset state + _buffer.Clear(); + _state.Sum = 0; + _state.WSum = 0; + _state.TickCount = 0; + + // Process window + for (int i = startIndex; i < len; i++) + { + double val = GetValidValue(source[i]); + UpdateState(val); + _state.LastInput = val; + } + + // Calculate Last + double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * 0.5; + Last = new TValue(DateTime.MinValue, _state.WSum / currentDivisor); + + _p_state = _state; + } + + public override void Reset() + { + _buffer.Clear(); + _state = default; + _p_state = default; + Last = default; + } + + public static TSeries Batch(TSeries source, int period) { var wma = new Wma(period); return wma.Update(source); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Calculate(ReadOnlySpan source, Span output, int period) + public static void Batch(ReadOnlySpan source, Span output, int period) { if (source.Length != output.Length) throw new ArgumentException("Source and output must have the same length"); @@ -286,12 +303,12 @@ public sealed class Wma : ITValuePublisher tickCount = 0; double recalcSum = 0; double recalcWsum = 0; - + for (int k = 0; k < period; k++) { int idx = bufferIdx + k; if (idx >= period) idx -= period; - + double v = buffer[idx]; recalcSum += v; recalcWsum += (k + 1) * v; @@ -744,12 +761,4 @@ public sealed class Wma : ITValuePublisher Unsafe.Add(ref outRef, idx) = wsum * invDivisor; } } - - public void Reset() - { - _buffer.Clear(); - _state = default; - _p_state = default; - Last = default; - } } diff --git a/lib/trends/wma/Wma.md b/lib/trends/wma/Wma.md index 26c09929..0ba398cf 100644 --- a/lib/trends/wma/Wma.md +++ b/lib/trends/wma/Wma.md @@ -81,12 +81,12 @@ Console.WriteLine($"IsHot: {wma.IsHot}"); // true when buffer is full // Batch calculation (TSeries API) TSeries source = ...; -TSeries results = Wma.Calculate(source, 10); +TSeries results = Wma.Batch(source, 10); // High-performance Span API (zero allocation) double[] prices = new double[10000]; double[] output = new double[10000]; -Wma.Calculate(prices.AsSpan(), output.AsSpan(), period: 10); +Wma.Batch(prices.AsSpan(), output.AsSpan(), period: 10); ``` ### Zero-Allocation Span API @@ -99,7 +99,7 @@ double[] source = new double[200000]; double[] wmaOutput = new double[200000]; // Zero heap allocation during calculation -Wma.Calculate(source.AsSpan(), wmaOutput.AsSpan(), period: 100); +Wma.Batch(source.AsSpan(), wmaOutput.AsSpan(), period: 100); // Results are written directly to output buffer Console.WriteLine($"Last WMA: {wmaOutput[^1]}"); diff --git a/lib/volatility/atr/Atr.Tests.cs b/lib/volatility/atr/Atr.Tests.cs new file mode 100644 index 00000000..9249798c --- /dev/null +++ b/lib/volatility/atr/Atr.Tests.cs @@ -0,0 +1,84 @@ +using System; +using Xunit; + +namespace QuanTAlib.Tests; + +public class AtrTests +{ + [Fact] + public void BasicCalculation_DoesNotCrash() + { + var atr = new Atr(14); + var gbm = new GBM(); + var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + + foreach (var bar in bars) + { + atr.Update(bar); + } + + Assert.True(double.IsFinite(atr.Last.Value)); + } + + [Fact] + public void IsNew_Consistency() + { + var atr = new Atr(14); + var gbm = new GBM(); + var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + + // Feed first 99 + for (int i = 0; i < 99; i++) + { + atr.Update(bars[i]); + } + + // Update with 100th point (isNew=true) + atr.Update(bars[99], true); + + // Update with modified 100th point (isNew=false) + var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 10.0, bars[99].Low - 10.0, bars[99].Close, bars[99].Volume); + + // This will update the logic: compute new TR based on modifiedBar vs prevBar(98) + double val2 = atr.Update(modifiedBar, false).Value; + + // Create new instance and feed up to modified + var atr2 = new Atr(14); + for (int i = 0; i < 99; i++) + { + atr2.Update(bars[i]); + } + double val3 = atr2.Update(modifiedBar, true).Value; + + Assert.Equal(val3, val2, 1e-9); + } + + [Fact] + public void Reset_Works() + { + var atr = new Atr(14); + var gbm = new GBM(); + var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + + foreach (var bar in bars) atr.Update(bar); + + double lastVal = atr.Last.Value; + Assert.NotEqual(0, lastVal); + + atr.Reset(); + Assert.Equal(0, atr.Last.Value); + Assert.False(atr.IsHot); + } + + [Fact] + public void Chainability_Works() + { + var atr = new Atr(14); + var gbm = new GBM(); + var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1)); + + var result = atr.Update(bars); + Assert.Equal(50, result.Count); + Assert.Equal(atr.Last.Value, result.Last.Value); + } +} diff --git a/lib/volatility/atr/Atr.Validation.Tests.cs b/lib/volatility/atr/Atr.Validation.Tests.cs new file mode 100644 index 00000000..ba5ff563 --- /dev/null +++ b/lib/volatility/atr/Atr.Validation.Tests.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Skender.Stock.Indicators; +using TALib; +using Xunit; +using QuanTAlib.Tests; + +namespace QuanTAlib; + +public sealed class AtrValidationTests : IDisposable +{ + private readonly ValidationTestData _data; + + public AtrValidationTests() + { + _data = new ValidationTestData(); + } + + public void Dispose() + { + _data.Dispose(); + } + + [Fact] + public void MatchesSkender() + { + var atr = new Atr(14); + var results = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + var res = atr.Update(_data.Bars[i]); + results.Add(res.Value); + } + + var skenderResults = _data.SkenderQuotes.GetAtr(14).ToList(); + + // ATR involves smoothing, so early values might differ slightly depending on initialization. + // Skender uses Wilder's initialization method. + ValidationHelper.VerifyData(results, skenderResults, x => x.Atr); + } + + [Fact] + public void MatchesTalib() + { + var atr = new Atr(14); + var results = new List(); + + for (int i = 0; i < _data.Bars.Count; i++) + { + var res = atr.Update(_data.Bars[i]); + results.Add(res.Value); + } + + double[] hData = _data.Bars.High.Select(x => x.Value).ToArray(); + double[] lData = _data.Bars.Low.Select(x => x.Value).ToArray(); + double[] cData = _data.Bars.Close.Select(x => x.Value).ToArray(); + double[] outReal = new double[_data.Bars.Count]; + + var retCode = TALib.Functions.Atr(hData, lData, cData, 0..^0, outReal, out var outRange, 14); + Assert.Equal(Core.RetCode.Success, retCode); + + int lookback = TALib.Functions.AtrLookback(14); + ValidationHelper.VerifyData(results, outReal, outRange, lookback); + } +} diff --git a/lib/volatility/atr/Atr.cs b/lib/volatility/atr/Atr.cs new file mode 100644 index 00000000..86637060 --- /dev/null +++ b/lib/volatility/atr/Atr.cs @@ -0,0 +1,199 @@ +using System.Runtime.CompilerServices; + +namespace QuanTAlib; + +/// +/// ATR: Average True Range +/// +/// +/// ATR measures the volatility of an asset. +/// It is the moving average (typically RMA/Wilder's) of the True Range. +/// +/// Calculation: +/// 1. True Range (TR) = Max(High - Low, |High - PrevClose|, |Low - PrevClose|) +/// - For the first bar, TR = High - Low +/// 2. ATR = RMA(TR) +/// +/// Sources: +/// "New Concepts in Technical Trading Systems" by J. Welles Wilder +/// +[SkipLocalsInit] +public sealed class Atr : AbstractBase +{ + private readonly Rma _rma; + private TBar _prevBar; + private bool _isInitialized; + + /// + /// Creates ATR with specified period. + /// + /// Period for ATR calculation (must be > 0) + public Atr(int period) + { + if (period <= 0) + throw new ArgumentException("Period must be greater than 0", nameof(period)); + + _rma = new Rma(period); + Name = $"Atr({period})"; + WarmupPeriod = period; + _isInitialized = false; + } + + /// + /// Creates ATR with specified source and period. + /// + /// Source to subscribe to + /// Period for ATR calculation + public Atr(ITValuePublisher source, int period) : this(period) + { + source.Pub += (item) => Update(item); + } + + /// + /// Creates ATR with specified source and period. + /// + public Atr(TBarSeries source, int period) : this(period) + { + var tr = CalculateTrueRange(source); + _rma.Prime(tr.Values); + Last = _rma.Last; + // We can't automatically subscribe to TBarSeries updates via this constructor + // because AbstractBase doesn't enforce TBarSeries subscription structure, + // but we can rely on manual updates or the user subscribing. + } + + /// + /// True if the ATR has warmed up and is providing valid results. + /// + public override bool IsHot => _rma.IsHot; + + /// + /// Initializes the indicator state using the provided history. + /// Note: ATR needs OHLCV data to calculate TR properly. + /// This Prime method expects pre-calculated TR values or handles basic priming + /// if the user erroneously passes non-TR data. Ideally, use Batched TBarSeries. + /// + public override void Prime(ReadOnlySpan source) + { + _rma.Prime(source); + Last = _rma.Last; + } + + /// + /// Resets the ATR state. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Reset() + { + _rma.Reset(); + _prevBar = default; + _isInitialized = false; + Last = default; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TValue Update(TBar input, bool isNew = true) + { + double tr; + if (!_isInitialized) + { + // For the very first bar, Wilder defines TR as High - Low + tr = input.High - input.Low; + } + else + { + // Calculate TR + double hl = input.High - input.Low; + double hpc = Math.Abs(input.High - _prevBar.Close); + double lpc = Math.Abs(input.Low - _prevBar.Close); + tr = Math.Max(hl, Math.Max(hpc, lpc)); + } + + if (isNew) + { + _prevBar = input; + _isInitialized = true; + } + + // Smooth TR using RMA + TValue result = _rma.Update(new TValue(input.Time, tr), isNew); + + Last = result; + PubEvent(Last); + return result; + } + + /// + /// Update for TValue input (not recommended for ATR as it needs OHLC). + /// This treats the input value as the TR itself. + /// + public override TValue Update(TValue input, bool isNew = true) + { + // If user passes a single value, we assume it IS the True Range + TValue result = _rma.Update(input, isNew); + Last = result; + PubEvent(Last); + return result; + } + + public TSeries Update(TBarSeries source) + { + if (source.Count == 0) return []; + + // 1. Calculate TR series + TSeries trSeries = CalculateTrueRange(source); + + // 2. Run RMA on TR + var result = _rma.Update(trSeries); + Last = _rma.Last; + + // 3. Synchronize state for subsequent updates + _prevBar = source.Last; + _isInitialized = true; + + return result; + } + + // AbstractBase.Update(TSeries) + public override TSeries Update(TSeries source) + { + // Assumes source is already TR + return _rma.Update(source); + } + + private static TSeries CalculateTrueRange(TBarSeries source) + { + var t = new List(source.Count); + var v = new List(source.Count); + + if (source.Count == 0) return new TSeries(t, v); + + // First bar TR = H - L + t.Add(source[0].Time); + v.Add(source[0].High - source[0].Low); + + for (int i = 1; i < source.Count; i++) + { + var bar = source[i]; + var prevBar = source[i - 1]; + + double hl = bar.High - bar.Low; + double hpc = Math.Abs(bar.High - prevBar.Close); + double lpc = Math.Abs(bar.Low - prevBar.Close); + double tr = Math.Max(hl, Math.Max(hpc, lpc)); + + t.Add(bar.Time); + v.Add(tr); + } + return new TSeries(t, v); + } + + /// + /// Calculates ATR for the entire series using a new instance. + /// + public static TSeries Batch(TBarSeries source, int period) + { + var atr = new Atr(period); + return atr.Update(source); + } +} diff --git a/quantower/IndicatorExtensions.Tests.cs b/quantower/IndicatorExtensions.Tests.cs index 6a3658fa..149c1502 100644 --- a/quantower/IndicatorExtensions.Tests.cs +++ b/quantower/IndicatorExtensions.Tests.cs @@ -242,6 +242,9 @@ public class IndicatorExtensionsTests // Test without cold values IndicatorExtensions.PaintSmoothCurve(indicator, args, series, warmupPeriod: 5, showColdValues: false); + + // Test PaintLine + IndicatorExtensions.PaintLine(indicator, args, series, warmupPeriod: 5, showColdValues: true); } // Test PaintHistogram with Positive and Negative values diff --git a/quantower/IndicatorExtensions.cs b/quantower/IndicatorExtensions.cs index b59cf2a5..649c90bd 100644 --- a/quantower/IndicatorExtensions.cs +++ b/quantower/IndicatorExtensions.cs @@ -1,287 +1,446 @@ -using TradingPlatform.BusinessLayer; -using TradingPlatform.BusinessLayer.Chart; -using System.Drawing; -using System.Drawing.Drawing2D; - -#nullable disable - -namespace QuanTAlib; - -public enum SourceType -{ - Open, High, Low, Close, HL2, OC2, OHL3, HLC3, OHLC4, HLCC4 -} - -public enum MaType -{ - Alma, Dema, Dsma, Dwma, Ema, Epma, Frama, Fwma, Gma, Hma, Hwma, Jma, Kama, Maaf, Mgdi, MMa, Pwma, Rema, Rma, Sinema, Sma, Smma, T3, Tema, Trima, Vidya, Wma, Zlema -} - -public static class IndicatorExtensions -{ - public static readonly Color Averages = Color.FromArgb(255, 255, 128); // #FFFF80 - Yellow - public static readonly Color Volume = Color.FromArgb(128, 255, 128); // #80FF80 - Green - public static readonly Color Volatility = Color.FromArgb(255, 128, 128); // #FF8080 - Red - public static readonly Color Statistics = Color.FromArgb(128, 128, 255); // #8080FF - Blue - public static readonly Color Oscillators = Color.FromArgb(255, 128, 255); // #FF80FF - Magenta - public static readonly Color Momentum = Color.FromArgb(128, 255, 255); // #80FFFF - Cyan - public static readonly Color Experiments = Color.FromArgb(255, 165, 0); // #FFA500 - Orange - - [AttributeUsage(AttributeTargets.Property)] - public class DataSourceInputAttribute : InputParameterAttribute - { - public DataSourceInputAttribute(string label = "Data source", int sortIndex = 20) - : base(label, sortIndex, variants: new object[] - { - "Open", SourceType.Open, - "High", SourceType.High, - "Low", SourceType.Low, - "Close", SourceType.Close, - "HL/2 (Median)", SourceType.HL2, - "OC/2 (Midpoint)", SourceType.OC2, - "OHL/3 (Mean)", SourceType.OHL3, - "HLC/3 (Typical)", SourceType.HLC3, - "OHLC/4 (Average)", SourceType.OHLC4, - "HLCC/4 (Weighted)", SourceType.HLCC4 - }) - { } - } - - public static TValue GetInputValue(this Indicator indicator, UpdateArgs args, SourceType source) - { - var historicalData = indicator.HistoricalData; - TBar bar = new TBar( - time: historicalData.Time(), - open: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Open], - high: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.High], - low: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Low], - close: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Close], - volume: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Volume] - ); - - double price = source switch - { - SourceType.Open => bar.Open, - SourceType.High => bar.High, - SourceType.Low => bar.Low, - SourceType.Close => bar.Close, - SourceType.HL2 => bar.HL2, - SourceType.OC2 => bar.OC2, - SourceType.OHL3 => bar.OHL3, - SourceType.HLC3 => bar.HLC3, - SourceType.OHLC4 => bar.OHLC4, - SourceType.HLCC4 => bar.HLCC4, - _ => bar.Close - }; - - return new TValue(bar.Time, price); - } - - public static TBar GetInputBar(this Indicator indicator, UpdateArgs args) - { - var historicalData = indicator.HistoricalData; - return new TBar( - time: historicalData.Time(), - open: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Open], - high: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.High], - low: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Low], - close: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Close], - volume: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Volume] - ); - } - -#pragma warning disable CA1416 // Validate platform compatibility - public static int GetHLineY(IChartWindowCoordinatesConverter converter, double value) - { - return (int)converter.GetChartY(value); - } - - public static void PaintHLine(this Indicator indicator, PaintChartEventArgs args, double value, Pen pen) - { - if (indicator.CurrentChart == null) - return; - - Graphics gr = args.Graphics; - var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; - var converter = mainWindow.CoordinatesConverter; - var clientRect = mainWindow.ClientRectangle; - - gr.SetClip(clientRect); - int leftX = clientRect.Left; - int rightX = clientRect.Right; - int Y = GetHLineY(converter, value); - - using (pen) - { - gr.DrawLine(pen, new Point(leftX, Y), new Point(rightX, Y)); - } - } - - public static List GetSmoothCurvePoints(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series) - { - ArgumentNullException.ThrowIfNull(indicator); - ArgumentNullException.ThrowIfNull(converter); - var data = indicator.HistoricalData; - if (data == null) return new List(); - - var lastTime = data.Time(data.Count - 1); - var firstTime = data.Time(0); - - IChartWindowCoordinatesConverter safeConverter = converter!; - DateTime tLeft = safeConverter.GetTime(clientRect.Left); - DateTime leftTime = tLeft > lastTime ? tLeft : lastTime; - - DateTime tRight = safeConverter.GetTime(clientRect.Right); - DateTime rightTime = tRight < firstTime ? tRight : firstTime; - - int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1; - int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); - - List allPoints = new List(); - - for (int i = rightIndex; i < leftIndex; i++) - { - int barX = (int)converter.GetChartX(data.Time(i)); - int barY = (int)converter.GetChartY(series[i]); - int halfBarWidth = indicator.CurrentChart.BarsWidth / 2; - Point point = new Point(barX + halfBarWidth, barY); - allPoints.Add(point); - } - return allPoints; - } - - public static void PaintSmoothCurve(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true, double tension = 0.2) - { - if (!series.Visible || indicator.CurrentChart == null) - return; - - Graphics gr = args.Graphics; - gr.SmoothingMode = SmoothingMode.AntiAlias; - var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; - var converter = mainWindow.CoordinatesConverter; - var clientRect = mainWindow.ClientRectangle; - - gr.SetClip(clientRect); - - List allPoints = GetSmoothCurvePoints(indicator, converter, clientRect, series); - - if (allPoints.Count > 1) - { - DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min(); - int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks); - - using Pen defaultPen = new(series.Color, series.Width) { DashStyle = ConvertLineStyleToDashStyle(series.Style) }; - using Pen coldPen = new(series.Color, series.Width) { DashStyle = DashStyle.Dot }; - - int hotCount = (warmupPeriod >= 0) ? (indicator.Count - warmupPeriod - rightIndex) : 0; - - // Draw the hot part - if (hotCount > 0) - { - var hotPoints = allPoints.Take(Math.Min(hotCount + 1, allPoints.Count)).ToArray(); - gr.DrawCurve(defaultPen, hotPoints, 0, hotPoints.Length - 1, (float)tension); - } - - // Draw the cold part - if (showColdValues && hotCount < allPoints.Count) - { - var coldPoints = allPoints.Skip(Math.Max(0, hotCount)).ToArray(); - gr.DrawCurve(coldPen, coldPoints, 0, coldPoints.Length - 1, (float)tension); - } - } - } - - public static List<(Rectangle Rect, Color Color)> GetHistogramRectangles(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series) - { - ArgumentNullException.ThrowIfNull(indicator); - ArgumentNullException.ThrowIfNull(converter); - var data = indicator.HistoricalData; - if (data == null) return new List<(Rectangle, Color)>(); - - var lastTime = data.Time(data.Count - 1); - var firstTime = data.Time(0); - - IChartWindowCoordinatesConverter safeConverter = converter!; - DateTime tLeft = safeConverter.GetTime(clientRect.Left); - DateTime leftTime = tLeft > lastTime ? tLeft : lastTime; - - DateTime tRight = safeConverter.GetTime(clientRect.Right); - DateTime rightTime = tRight < firstTime ? tRight : firstTime; - - int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1; - int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); - - var result = new List<(Rectangle, Color)>(); - - for (int i = rightIndex; i < leftIndex; i++) - { - int barX = (int)converter.GetChartX(data.Time(i)); - int barY = (int)converter.GetChartY(series[i]); - int barY0 = (int)converter.GetChartY(0); - int HistBarWidth = indicator.CurrentChart.BarsWidth - 2; - - if (series[i] > 0) - { - result.Add((new Rectangle(barX, barY, HistBarWidth, Math.Abs(barY - barY0)), Color.FromArgb(150, 0, 255, 0))); - } - else - { - result.Add((new Rectangle(barX, barY0, HistBarWidth, Math.Abs(barY0 - barY)), Color.FromArgb(150, 255, 0, 0))); - } - } - return result; - } - - public static void PaintHistogram(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true) - { - if (!series.Visible || indicator.CurrentChart == null) - return; - - Graphics gr = args.Graphics; - gr.SmoothingMode = SmoothingMode.AntiAlias; - var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; - var converter = mainWindow.CoordinatesConverter; - var clientRect = mainWindow.ClientRectangle; - - gr.SetClip(clientRect); - - var rects = GetHistogramRectangles(indicator, converter, clientRect, series); - - foreach (var (rect, color) in rects) - { - using (Brush hist = new SolidBrush(color)) - { - gr.FillRectangle(hist, rect); - } - } - } - - public static void DrawText(this Indicator indicator, PaintChartEventArgs args, string text) - { - if (indicator.CurrentChart == null) - return; - - Graphics gr = args.Graphics; - var clientRect = indicator.CurrentChart.MainWindow.ClientRectangle; - Font font = new Font("Inter", 8); - SizeF textSize = gr.MeasureString(text, font); - RectangleF textRect = new RectangleF(clientRect.Left + 5, - clientRect.Bottom - textSize.Height - 10, - textSize.Width + 10, textSize.Height + 10); - - gr.FillRectangle(Brushes.DarkBlue, textRect); - gr.DrawString(text, font, Brushes.White, new PointF(textRect.X + 6, textRect.Y + 5)); - } - - private static DashStyle ConvertLineStyleToDashStyle(LineStyle lineStyle) - { - return lineStyle switch - { - LineStyle.Solid => DashStyle.Solid, - LineStyle.Dash => DashStyle.Dash, - LineStyle.Dot => DashStyle.Dot, - LineStyle.DashDot => DashStyle.DashDot, - _ => DashStyle.Solid, - }; - } -} +using TradingPlatform.BusinessLayer; +using TradingPlatform.BusinessLayer.Chart; +using System.Drawing; +using System.Drawing.Drawing2D; + +#nullable disable + +namespace QuanTAlib; + +public enum SourceType +{ + Open, High, Low, Close, HL2, OC2, OHL3, HLC3, OHLC4, HLCC4 +} + +public enum MaType +{ + Alma, Dema, Dsma, Dwma, Ema, Epma, Frama, Fwma, Gma, Hma, Hwma, Jma, Kama, Maaf, Mgdi, MMa, Pwma, Rema, Rma, Sinema, Sma, Smma, T3, Tema, Trima, Vidya, Wma, Zlema +} + +public static class IndicatorExtensions +{ + public static readonly Color Averages = Color.FromArgb(255, 255, 128); // #FFFF80 - Yellow + public static readonly Color Volume = Color.FromArgb(128, 255, 128); // #80FF80 - Green + public static readonly Color Volatility = Color.FromArgb(255, 128, 128); // #FF8080 - Red + public static readonly Color Statistics = Color.FromArgb(128, 128, 255); // #8080FF - Blue + public static readonly Color Oscillators = Color.FromArgb(255, 128, 255); // #FF80FF - Magenta + public static readonly Color Momentum = Color.FromArgb(128, 255, 255); // #80FFFF - Cyan + public static readonly Color Experiments = Color.FromArgb(255, 165, 0); // #FFA500 - Orange + + [AttributeUsage(AttributeTargets.Property)] + public class DataSourceInputAttribute : InputParameterAttribute + { + public DataSourceInputAttribute(string label = "Data source", int sortIndex = 20) + : base(label, sortIndex, variants: new object[] + { + "Open", SourceType.Open, + "High", SourceType.High, + "Low", SourceType.Low, + "Close", SourceType.Close, + "HL/2 (Median)", SourceType.HL2, + "OC/2 (Midpoint)", SourceType.OC2, + "OHL/3 (Mean)", SourceType.OHL3, + "HLC/3 (Typical)", SourceType.HLC3, + "OHLC/4 (Average)", SourceType.OHLC4, + "HLCC/4 (Weighted)", SourceType.HLCC4 + }) + { } + } + + public static TValue GetInputValue(this Indicator indicator, UpdateArgs args, SourceType source) + { + var historicalData = indicator.HistoricalData; + var item = historicalData[indicator.Count - 1, SeekOriginHistory.Begin]; + double price = item.GetPrice(source); + return new TValue(item.TimeLeft.Ticks, price); + } + + public static TBar GetInputBar(this Indicator indicator, UpdateArgs args) + { + var historicalData = indicator.HistoricalData; + return new TBar( + time: historicalData.Time(), + open: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Open], + high: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.High], + low: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Low], + close: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Close], + volume: historicalData[indicator.Count - 1, SeekOriginHistory.Begin][PriceType.Volume] + ); + } + + public static double GetPrice(this IHistoryItem item, SourceType source) + { + return source switch + { + SourceType.Open => item[PriceType.Open], + SourceType.High => item[PriceType.High], + SourceType.Low => item[PriceType.Low], + SourceType.Close => item[PriceType.Close], + SourceType.HL2 => (item[PriceType.High] + item[PriceType.Low]) * 0.5, + SourceType.OC2 => (item[PriceType.Open] + item[PriceType.Close]) * 0.5, + SourceType.OHL3 => (item[PriceType.Open] + item[PriceType.High] + item[PriceType.Low]) * 0.333333333333333333, + SourceType.HLC3 => (item[PriceType.High] + item[PriceType.Low] + item[PriceType.Close]) * 0.333333333333333333, + SourceType.OHLC4 => (item[PriceType.Open] + item[PriceType.High] + item[PriceType.Low] + item[PriceType.Close]) * 0.25, + SourceType.HLCC4 => (item[PriceType.High] + item[PriceType.Low] + item[PriceType.Close] + item[PriceType.Close]) * 0.25, + _ => item[PriceType.Close] + }; + } + + public static void FillValues(this HistoricalData history, Span destination, SourceType source) + { + int count = Math.Min(history.Count, destination.Length); + + // Hoist switch to avoid per-iteration branching + switch (source) + { + case SourceType.Open: + for (int i = 0; i < count; i++) destination[i] = history[i, SeekOriginHistory.Begin][PriceType.Open]; + break; + case SourceType.High: + for (int i = 0; i < count; i++) destination[i] = history[i, SeekOriginHistory.Begin][PriceType.High]; + break; + case SourceType.Low: + for (int i = 0; i < count; i++) destination[i] = history[i, SeekOriginHistory.Begin][PriceType.Low]; + break; + case SourceType.Close: + for (int i = 0; i < count; i++) destination[i] = history[i, SeekOriginHistory.Begin][PriceType.Close]; + break; + case SourceType.HL2: + for (int i = 0; i < count; i++) + { + var item = history[i, SeekOriginHistory.Begin]; + destination[i] = (item[PriceType.High] + item[PriceType.Low]) * 0.5; + } + break; + case SourceType.OC2: + for (int i = 0; i < count; i++) + { + var item = history[i, SeekOriginHistory.Begin]; + destination[i] = (item[PriceType.Open] + item[PriceType.Close]) * 0.5; + } + break; + case SourceType.OHL3: + for (int i = 0; i < count; i++) + { + var item = history[i, SeekOriginHistory.Begin]; + destination[i] = (item[PriceType.Open] + item[PriceType.High] + item[PriceType.Low]) * 0.333333333333333333; + } + break; + case SourceType.HLC3: + for (int i = 0; i < count; i++) + { + var item = history[i, SeekOriginHistory.Begin]; + destination[i] = (item[PriceType.High] + item[PriceType.Low] + item[PriceType.Close]) * 0.333333333333333333; + } + break; + case SourceType.OHLC4: + for (int i = 0; i < count; i++) + { + var item = history[i, SeekOriginHistory.Begin]; + destination[i] = (item[PriceType.Open] + item[PriceType.High] + item[PriceType.Low] + item[PriceType.Close]) * 0.25; + } + break; + case SourceType.HLCC4: + for (int i = 0; i < count; i++) + { + var item = history[i, SeekOriginHistory.Begin]; + destination[i] = (item[PriceType.High] + item[PriceType.Low] + item[PriceType.Close] + item[PriceType.Close]) * 0.25; + } + break; + default: + for (int i = 0; i < count; i++) destination[i] = history[i, SeekOriginHistory.Begin][PriceType.Close]; + break; + } + } + + public static void SetValues(this LineSeries series, ReadOnlySpan values) + { + int count = values.Length; + for (int i = 0; i < count; i++) + { + series.SetValue(values[i], i, SeekOriginHistory.Begin); + } + } + +#pragma warning disable CA1416 // Validate platform compatibility + public static int GetHLineY(IChartWindowCoordinatesConverter converter, double value) + { + return (int)converter.GetChartY(value); + } + + public static void PaintHLine(this Indicator indicator, PaintChartEventArgs args, double value, Pen pen) + { + if (indicator.CurrentChart == null) + return; + + Graphics gr = args.Graphics; + var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; + var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; + + gr.SetClip(clientRect); + int leftX = clientRect.Left; + int rightX = clientRect.Right; + int Y = GetHLineY(converter, value); + + using (pen) + { + gr.DrawLine(pen, new Point(leftX, Y), new Point(rightX, Y)); + } + } + + public static Point[] GetSmoothCurvePoints(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series) + { + ArgumentNullException.ThrowIfNull(indicator); + ArgumentNullException.ThrowIfNull(converter); + var data = indicator.HistoricalData; + if (data == null) return Array.Empty(); + + var lastTime = data.Time(data.Count - 1); + var firstTime = data.Time(0); + + IChartWindowCoordinatesConverter safeConverter = converter!; + DateTime tLeft = safeConverter.GetTime(clientRect.Left); + DateTime leftTime = tLeft > lastTime ? tLeft : lastTime; + + DateTime tRight = safeConverter.GetTime(clientRect.Right); + DateTime rightTime = tRight < firstTime ? tRight : firstTime; + + int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1; + int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); + + int count = leftIndex - rightIndex; + if (count <= 0) return Array.Empty(); + + Point[] allPoints = new Point[count]; + + for (int i = 0; i < count; i++) + { + int dataIndex = rightIndex + i; + int barX = (int)converter.GetChartX(data.Time(dataIndex)); + int barY = (int)converter.GetChartY(series[dataIndex]); + int halfBarWidth = indicator.CurrentChart.BarsWidth / 2; + allPoints[i] = new Point(barX + halfBarWidth, barY); + } + return allPoints; + } + + public static void PaintSmoothCurve(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true, double tension = 0.2) + { + if (!series.Visible || indicator.CurrentChart == null) + return; + + Graphics gr = args.Graphics; + gr.SmoothingMode = SmoothingMode.AntiAlias; + var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; + var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; + + gr.SetClip(clientRect); + + Point[] allPoints = GetSmoothCurvePoints(indicator, converter, clientRect, series); + + if (allPoints.Length > 1) + { + DateTime tRight = converter.GetTime(clientRect.Right); + DateTime tZero = indicator.HistoricalData.Time(0); + DateTime rightTime = tRight < tZero ? tRight : tZero; + + int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks); + + using Pen defaultPen = new(series.Color, series.Width) { DashStyle = ConvertLineStyleToDashStyle(series.Style) }; + using Pen coldPen = new(series.Color, series.Width) { DashStyle = DashStyle.Dot }; + + int hotCount = (warmupPeriod >= 0) ? (indicator.Count - warmupPeriod - rightIndex) : 0; + + // Draw the hot part + int hotSegments = Math.Min(hotCount, allPoints.Length - 1); + if (hotSegments > 0) + { + gr.DrawCurve(defaultPen, allPoints, 0, hotSegments, (float)tension); + } + + // Draw the cold part + if (showColdValues) + { + int coldStart = Math.Max(0, hotCount); + int coldSegments = (allPoints.Length - 1) - coldStart; + + if (coldSegments > 0) + { + gr.DrawCurve(coldPen, allPoints, coldStart, coldSegments, (float)tension); + } + } + } + } + + public static void PaintLine(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true) + { + if (!series.Visible || indicator.CurrentChart == null) + return; + + Graphics gr = args.Graphics; + gr.SmoothingMode = SmoothingMode.AntiAlias; + var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; + var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; + + gr.SetClip(clientRect); + + var data = indicator.HistoricalData; + if (data == null) return; + + var lastTime = data.Time(data.Count - 1); + var firstTime = data.Time(0); + + IChartWindowCoordinatesConverter safeConverter = converter!; + DateTime tLeft = safeConverter.GetTime(clientRect.Left); + DateTime leftTime = tLeft > lastTime ? tLeft : lastTime; + + DateTime tRight = safeConverter.GetTime(clientRect.Right); + DateTime rightTime = tRight < firstTime ? tRight : firstTime; + + int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1; + int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); + + int count = leftIndex - rightIndex; + if (count <= 0) return; + + // Use ArrayPool to avoid allocations + Point[] allPoints = System.Buffers.ArrayPool.Shared.Rent(count); + try + { + int halfBarWidth = indicator.CurrentChart.BarsWidth / 2; + for (int i = 0; i < count; i++) + { + int dataIndex = rightIndex + i; + int barX = (int)converter.GetChartX(data.Time(dataIndex)); + int barY = (int)converter.GetChartY(series[dataIndex]); + allPoints[i] = new Point(barX + halfBarWidth, barY); + } + + if (count > 1) + { + using Pen defaultPen = new(series.Color, series.Width) { DashStyle = ConvertLineStyleToDashStyle(series.Style) }; + using Pen coldPen = new(series.Color, series.Width) { DashStyle = DashStyle.Dot }; + + int hotCount = (warmupPeriod >= 0) ? (indicator.Count - warmupPeriod - rightIndex) : 0; + + // Draw the hot part + int hotSegments = Math.Min(hotCount, count - 1); + if (hotSegments > 0) + { + gr.DrawCurve(defaultPen, allPoints, 0, hotSegments, tension: 0); + } + + // Draw the cold part + if (showColdValues) + { + int coldStart = Math.Max(0, hotCount); + int coldSegments = (count - 1) - coldStart; + + if (coldSegments > 0) + { + gr.DrawCurve(coldPen, allPoints, coldStart, coldSegments, tension: 0); + } + } + } + } + finally + { + System.Buffers.ArrayPool.Shared.Return(allPoints); + } + } + + public static List<(Rectangle Rect, Color Color)> GetHistogramRectangles(Indicator indicator, IChartWindowCoordinatesConverter converter, Rectangle clientRect, LineSeries series) + { + ArgumentNullException.ThrowIfNull(indicator); + ArgumentNullException.ThrowIfNull(converter); + var data = indicator.HistoricalData; + if (data == null) return new List<(Rectangle, Color)>(); + + var lastTime = data.Time(data.Count - 1); + var firstTime = data.Time(0); + + IChartWindowCoordinatesConverter safeConverter = converter!; + DateTime tLeft = safeConverter.GetTime(clientRect.Left); + DateTime leftTime = tLeft > lastTime ? tLeft : lastTime; + + DateTime tRight = safeConverter.GetTime(clientRect.Right); + DateTime rightTime = tRight < firstTime ? tRight : firstTime; + + int leftIndex = (int)data.GetIndexByTime(leftTime.Ticks) + 1; + int rightIndex = (int)data.GetIndexByTime(rightTime.Ticks); + + var result = new List<(Rectangle, Color)>(); + + for (int i = rightIndex; i < leftIndex; i++) + { + int barX = (int)converter.GetChartX(data.Time(i)); + int barY = (int)converter.GetChartY(series[i]); + int barY0 = (int)converter.GetChartY(0); + int HistBarWidth = indicator.CurrentChart.BarsWidth - 2; + + if (series[i] > 0) + { + result.Add((new Rectangle(barX, barY, HistBarWidth, Math.Abs(barY - barY0)), Color.FromArgb(150, 0, 255, 0))); + } + else + { + result.Add((new Rectangle(barX, barY0, HistBarWidth, Math.Abs(barY0 - barY)), Color.FromArgb(150, 255, 0, 0))); + } + } + return result; + } + + public static void PaintHistogram(this Indicator indicator, PaintChartEventArgs args, LineSeries series, int warmupPeriod, bool showColdValues = true) + { + if (!series.Visible || indicator.CurrentChart == null) + return; + + Graphics gr = args.Graphics; + gr.SmoothingMode = SmoothingMode.AntiAlias; + var mainWindow = indicator.CurrentChart.Windows[args.WindowIndex]; + var converter = mainWindow.CoordinatesConverter; + var clientRect = mainWindow.ClientRectangle; + + gr.SetClip(clientRect); + + var rects = GetHistogramRectangles(indicator, converter, clientRect, series); + + foreach (var (rect, color) in rects) + { + using Brush hist = new SolidBrush(color); + gr.FillRectangle(hist, rect); + } + } + + public static void DrawText(this Indicator indicator, PaintChartEventArgs args, string text) + { + if (indicator.CurrentChart == null) + return; + + Graphics gr = args.Graphics; + var clientRect = indicator.CurrentChart.MainWindow.ClientRectangle; + var font = new Font("Inter", 8); + SizeF textSize = gr.MeasureString(text, font); + var textRect = new RectangleF(clientRect.Left + 5, + clientRect.Bottom - textSize.Height - 10, + textSize.Width + 10, textSize.Height + 10); + + gr.FillRectangle(Brushes.DarkBlue, textRect); + gr.DrawString(text, font, Brushes.White, new PointF(textRect.X + 6, textRect.Y + 5)); + } + + private static DashStyle ConvertLineStyleToDashStyle(LineStyle lineStyle) + { + return lineStyle switch + { + LineStyle.Solid => DashStyle.Solid, + LineStyle.Dash => DashStyle.Dash, + LineStyle.Dot => DashStyle.Dot, + LineStyle.DashDot => DashStyle.DashDot, + _ => DashStyle.Solid, + }; + } +} diff --git a/quantower/Mocks/TradingPlatformMocks.cs b/quantower/Mocks/TradingPlatformMocks.cs index ceaac5e7..41f56392 100644 --- a/quantower/Mocks/TradingPlatformMocks.cs +++ b/quantower/Mocks/TradingPlatformMocks.cs @@ -2,379 +2,371 @@ // These are minimal implementations for unit testing purposes only using System.Drawing; +using TradingPlatform.BusinessLayer.Chart; namespace TradingPlatform.BusinessLayer { - using TradingPlatform.BusinessLayer.Chart; + namespace Chart + { + /// + /// Coordinates converter interface + /// + public interface IChartWindowCoordinatesConverter + { + DateTime GetTime(int x); + double GetChartX(DateTime time); + double GetChartY(double value); + } + } #region Enums -/// -/// Specifies the style of indicator line. -/// -public enum LineStyle -{ - Solid, - Dash, - Dot, - DashDot, - Histogramm, - Points, - Columns, - StepLine -} + /// + /// Specifies the style of indicator line. + /// + public enum LineStyle + { + Solid, + Dash, + Dot, + DashDot, + Histogramm, + Points, + Columns, + StepLine + } -/// -/// Price data types -/// -public enum PriceType -{ - Open, - High, - Low, - Close, - Median, - Typical, - Weighted, - Bid, - BidSize, - Ask, - AskSize, - Last, - Volume, - Ticks, - AggressorFlag, - TickDirection, - BidTickDirection, - AskTickDirection, - OpenInterest, - Mark, - FundingRate, - QuoteAssetVolume -} + /// + /// Price data types + /// + public enum PriceType + { + Open, + High, + Low, + Close, + Median, + Typical, + Weighted, + Bid, + BidSize, + Ask, + AskSize, + Last, + Volume, + Ticks, + AggressorFlag, + TickDirection, + BidTickDirection, + AskTickDirection, + OpenInterest, + Mark, + FundingRate, + QuoteAssetVolume + } -/// -/// Seek origin for historical data -/// -public enum SeekOriginHistory -{ - Begin, - End -} + /// + /// Seek origin for historical data + /// + public enum SeekOriginHistory + { + Begin, + End + } -/// -/// Update reason for indicator -/// -public enum UpdateReason -{ - Unknown, - HistoricalBar, - NewTick, - NewBar -} + /// + /// Update reason for indicator + /// + public enum UpdateReason + { + Unknown, + HistoricalBar, + NewTick, + NewBar + } -#endregion + /// + /// Indicator line marker icon type + /// + public enum IndicatorLineMarkerIconType + { + None, + Point, + Circle, + Square, + Diamond, + Triangle, + TriangleDown, + Cross, + Plus, + Star, + Flag, + ArrowUp, + ArrowDown, + ArrowLeft, + ArrowRight + } -#region Attributes + #endregion -/// -/// Attribute for input parameters -/// -[AttributeUsage(AttributeTargets.Property)] -public class InputParameterAttribute : Attribute -{ - public string Name { get; } - public int SortIndex { get; } - public double Minimum { get; } - public double Maximum { get; } - public double Increment { get; } - public int DecimalPlaces { get; } - public IComparable[]? Variants { get; } + #region Attributes - public InputParameterAttribute( + /// + /// Attribute for input parameters + /// + [AttributeUsage(AttributeTargets.Property)] + public class InputParameterAttribute( string name = "", int sortIndex = 0, double minimum = int.MinValue, double maximum = int.MaxValue, double increment = 0.01, int decimalPlaces = 2, - object[]? variants = null) + object[]? variants = null) : Attribute { - Name = name; - SortIndex = sortIndex; - Minimum = minimum; - Maximum = maximum; - Increment = increment; - DecimalPlaces = decimalPlaces; - Variants = variants?.Cast().ToArray(); + public string Name { get; } = name; + public int SortIndex { get; } = sortIndex; + public double Minimum { get; } = minimum; + public double Maximum { get; } = maximum; + public double Increment { get; } = increment; + public int DecimalPlaces { get; } = decimalPlaces; + public IComparable[]? Variants { get; } = variants?.Cast().ToArray(); } -} -#endregion + #endregion -#region History Item + #region History Item -/// -/// History item interface -/// -public interface IHistoryItem -{ - DateTime TimeLeft { get; } - long TicksLeft { get; set; } - long TicksRight { get; set; } - double this[PriceType priceType] { get; } -} - -/// -/// Mock history item for testing -/// -public class MockHistoryItem : IHistoryItem -{ - public DateTime TimeLeft { get; set; } - public long TicksLeft { get; set; } - public long TicksRight { get; set; } - public double Open { get; set; } - public double High { get; set; } - public double Low { get; set; } - public double Close { get; set; } - public double Volume { get; set; } - - public double this[PriceType priceType] => priceType switch + /// + /// History item interface + /// + public interface IHistoryItem { - PriceType.Open => Open, - PriceType.High => High, - PriceType.Low => Low, - PriceType.Close => Close, - PriceType.Volume => Volume, - PriceType.Median => (High + Low) / 2, - PriceType.Typical => (High + Low + Close) / 3, - PriceType.Weighted => (High + Low + Close + Close) / 4, - _ => Close - }; -} + DateTime TimeLeft { get; } + long TicksLeft { get; set; } + long TicksRight { get; set; } + double this[PriceType priceType] { get; } + } -#endregion - -#region Historical Data - -/// -/// Mock historical data for testing -/// -public class HistoricalData -{ - private readonly List _items = new(); - - public int Count => _items.Count; - - public IHistoryItem this[int offset, SeekOriginHistory origin = SeekOriginHistory.End] + /// + /// Mock history item for testing + /// + public class MockHistoryItem : IHistoryItem { - get + public DateTime TimeLeft { get; set; } + public long TicksLeft { get; set; } + public long TicksRight { get; set; } + public double Open { get; set; } + public double High { get; set; } + public double Low { get; set; } + public double Close { get; set; } + public double Volume { get; set; } + + public double this[PriceType priceType] => priceType switch { + PriceType.Open => Open, + PriceType.High => High, + PriceType.Low => Low, + PriceType.Close => Close, + PriceType.Volume => Volume, + PriceType.Median => (High + Low) / 2, + PriceType.Typical => (High + Low + Close) / 3, + PriceType.Weighted => (High + Low + Close + Close) / 4, + _ => Close + }; + } + + #endregion + + #region Historical Data + + /// + /// Mock historical data for testing + /// + public class HistoricalData + { + private readonly List _items = []; + + public int Count => _items.Count; + + public IHistoryItem this[int offset, SeekOriginHistory origin = SeekOriginHistory.End] + { + get + { + int index = origin == SeekOriginHistory.End + ? Count - 1 - offset + : offset; + return _items[index]; + } + } + + public DateTime Time(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) + { + return this[offset, origin].TimeLeft; + } + + public long GetIndexByTime(long ticks) + { + for (int i = 0; i < _items.Count; i++) + { + if (_items[i].TicksLeft == ticks) + return Count - 1 - i; + } + return -1; + } + + public void Add(IHistoryItem item) + { + _items.Add(item); + } + + public void AddBar(DateTime time, double open, double high, double low, double close, double volume = 0) + { + _items.Add(new MockHistoryItem + { + TimeLeft = time, + TicksLeft = time.Ticks, + TicksRight = time.Ticks, + Open = open, + High = high, + Low = low, + Close = close, + Volume = volume + }); + } + + public void Clear() => _items.Clear(); + } + + #endregion + + #region Update Args + + /// + /// Update arguments for indicator + /// + public class UpdateArgs(UpdateReason reason) + { + public UpdateReason Reason { get; } = reason; + } + + #endregion + + #region Line Series + + /// + /// Base class for lines + /// + public class IndicatorLineMarker(Color color, IndicatorLineMarkerIconType icon = IndicatorLineMarkerIconType.None) + { + public Color Color { get; set; } = color; + public IndicatorLineMarkerIconType Icon { get; set; } = icon; + } + + public class Line(string name, Color color, int width, LineStyle style) + { + public string Name { get; set; } = name; + public Color Color { get; set; } = color; + public int Width { get; set; } = width; + public LineStyle Style { get; set; } = style; + public bool Visible { get; set; } = true; + } + + /// + /// Line series for indicator output + /// + public class LineSeries(string name, Color color, int width, LineStyle style) + : Line(name, color, width, style) + { + private readonly List _values = []; + private readonly List _markers = []; + + public int TimeShift { get; set; } + public int DrawBegin { get; set; } + public bool ShowLineMarker { get; set; } = true; + + public double this[int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End] + { + get => GetValue(offset, origin); + set => SetValue(value, offset, origin); + } + + public double GetValue(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) + { + if (_values.Count == 0) + return double.NaN; + int index = origin == SeekOriginHistory.End - ? Count - 1 - offset + ? _values.Count - 1 - offset : offset; - return _items[index]; + + if (index < 0 || index >= _values.Count) + return double.NaN; + + return _values[index]; } - } - public DateTime Time(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) - { - return this[offset, origin].TimeLeft; - } - - public long GetIndexByTime(long ticks) - { - for (int i = 0; i < _items.Count; i++) + public void SetValue(double value, int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) { - if (_items[i].TicksLeft == ticks) - return Count - 1 - i; + EnsureCapacity(offset + 1); + int index = origin == SeekOriginHistory.End + ? _values.Count - 1 - offset + : offset; + _values[index] = value; } - return -1; - } - public void Add(IHistoryItem item) - { - _items.Add(item); - } - - public void AddBar(DateTime time, double open, double high, double low, double close, double volume = 0) - { - _items.Add(new MockHistoryItem + public void SetMarker(int offset, Color color) { - TimeLeft = time, - TicksLeft = time.Ticks, - TicksRight = time.Ticks, - Open = open, - High = high, - Low = low, - Close = close, - Volume = volume - }); - } + EnsureMarkerCapacity(offset + 1); + int index = _markers.Count - 1 - offset; + if (index >= 0 && index < _markers.Count) + _markers[index] = color; + } - public void Clear() => _items.Clear(); -} + public void SetMarker(int offset, IndicatorLineMarker marker) + { + SetMarker(offset, marker.Color); + } -#endregion - -#region Update Args - -/// -/// Update arguments for indicator -/// -public class UpdateArgs -{ - public UpdateReason Reason { get; } - - public UpdateArgs(UpdateReason reason) - { - Reason = reason; - } -} - -#endregion - -#region Line Series - -/// -/// Base class for lines -/// -public abstract class Line -{ - public string Name { get; set; } - public Color Color { get; set; } - public int Width { get; set; } - public LineStyle Style { get; set; } - public bool Visible { get; set; } = true; - - protected Line(string name, Color color, int width, LineStyle style) - { - Name = name; - Color = color; - Width = width; - Style = style; - } -} - -/// -/// Line series for indicator output -/// -public class LineSeries : Line -{ - private readonly List _values = new(); - private readonly List _markers = new(); - - public int TimeShift { get; set; } - public int DrawBegin { get; set; } - public bool ShowLineMarker { get; set; } = true; - - public LineSeries(string name, Color color, int width, LineStyle style) - : base(name, color, width, style) - { - } - - public double this[int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End] - { - get => GetValue(offset, origin); - set => SetValue(value, offset, origin); - } - - public double GetValue(int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) - { - if (_values.Count == 0) - return double.NaN; - - int index = origin == SeekOriginHistory.End - ? _values.Count - 1 - offset - : offset; - - if (index < 0 || index >= _values.Count) - return double.NaN; - - return _values[index]; - } - - public void SetValue(double value, int offset = 0, SeekOriginHistory origin = SeekOriginHistory.End) - { - EnsureCapacity(offset + 1); - int index = origin == SeekOriginHistory.End - ? _values.Count - 1 - offset - : offset; - _values[index] = value; - } - - public void SetMarker(int offset, Color color) - { - EnsureMarkerCapacity(offset + 1); - int index = _markers.Count - 1 - offset; - if (index >= 0 && index < _markers.Count) - _markers[index] = color; - } - - internal void AddValue() - { - _values.Add(double.NaN); - _markers.Add(Color.Transparent); - } - - private void EnsureCapacity(int count) - { - while (_values.Count < count) + internal void AddValue() + { _values.Add(double.NaN); - } - - private void EnsureMarkerCapacity(int count) - { - while (_markers.Count < count) _markers.Add(Color.Transparent); + } + + private void EnsureCapacity(int count) + { + while (_values.Count < count) + _values.Add(double.NaN); + } + + private void EnsureMarkerCapacity(int count) + { + while (_markers.Count < count) + _markers.Add(Color.Transparent); + } + + public int Count => _values.Count; + public IReadOnlyList Values => _values; } - public int Count => _values.Count; - public IReadOnlyList Values => _values; -} + #endregion -#endregion + #region Paint Chart Event Args -#region Paint Chart Event Args - -/// -/// Paint chart event arguments -/// -public class PaintChartEventArgs : EventArgs -{ - public Graphics Graphics { get; } - public Rectangle ClipRectangle { get; } - public int WindowIndex { get; } - - public PaintChartEventArgs(Graphics graphics, Rectangle clipRectangle, int windowIndex = 0) + /// + /// Paint chart event arguments + /// + public class PaintChartEventArgs(Graphics graphics, Rectangle clipRectangle, int windowIndex = 0) : EventArgs { - Graphics = graphics; - ClipRectangle = clipRectangle; - WindowIndex = windowIndex; + public Graphics Graphics { get; } = graphics; + public Rectangle ClipRectangle { get; } = clipRectangle; + public int WindowIndex { get; } = windowIndex; } -} -#endregion + #endregion #region Chart -} - -namespace TradingPlatform.BusinessLayer.Chart -{ - /// - /// Coordinates converter interface - /// - public interface IChartWindowCoordinatesConverter - { - DateTime GetTime(int x); - double GetChartX(DateTime time); - double GetChartY(double value); - } -} - -namespace TradingPlatform.BusinessLayer -{ - using TradingPlatform.BusinessLayer.Chart; /// /// Chart interface @@ -382,7 +374,7 @@ namespace TradingPlatform.BusinessLayer public interface IChart { ChartWindow MainWindow { get; } - ChartWindow[] Windows { get; } + IList Windows { get; } int BarsWidth { get; } } @@ -411,7 +403,7 @@ namespace TradingPlatform.BusinessLayer public class MockChart : IChart { public ChartWindow MainWindow { get; } = new(); - public ChartWindow[] Windows { get; } = new[] { new ChartWindow() }; + public IList Windows { get; } = [new ChartWindow()]; public int BarsWidth { get; set; } = 10; } @@ -419,83 +411,86 @@ namespace TradingPlatform.BusinessLayer #region Indicator Base -/// -/// Watchlist indicator interface -/// -public interface IWatchlistIndicator -{ - int MinHistoryDepths { get; } -} - -/// -/// Base class for indicators -/// -public abstract class Indicator -{ - private readonly List _lineSeries = new(); - - public string Name { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - public virtual string ShortName => Name; - public virtual string SourceCodeLink => string.Empty; - - public bool SeparateWindow { get; set; } - public bool OnBackGround { get; set; } - - public HistoricalData HistoricalData { get; set; } = new(); - public IChart? CurrentChart { get; set; } - - public int Count => HistoricalData.Count; - - public LineSeries[] LinesSeries => _lineSeries.ToArray(); - - protected void AddLineSeries(LineSeries series) + /// + /// Watchlist indicator interface + /// + public interface IWatchlistIndicator { - _lineSeries.Add(series); + int MinHistoryDepths { get; } } /// - /// Called when indicator is initialized + /// Base class for indicators /// - protected virtual void OnInit() + public abstract class Indicator { - } + private readonly List _lineSeries = []; - /// - /// Called on each update - /// - protected virtual void OnUpdate(UpdateArgs args) - { - } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public virtual string ShortName => Name; + public virtual string SourceCodeLink => string.Empty; - /// - /// Called for chart painting - /// - public virtual void OnPaintChart(PaintChartEventArgs args) - { - } + public bool SeparateWindow { get; set; } + public bool OnBackGround { get; set; } - /// - /// Initialize the indicator (for testing) - /// - public void Initialize() - { - OnInit(); - } + public HistoricalData HistoricalData { get; set; } = new(); + public IChart? CurrentChart { get; set; } - /// - /// Process an update (for testing) - /// - public void ProcessUpdate(UpdateArgs args) - { - // Ensure line series have capacity for new data - foreach (var series in _lineSeries) + public int Count => HistoricalData.Count; + + public IList LinesSeries => _lineSeries.ToArray(); + + protected void AddLineSeries(LineSeries series) { - series.AddValue(); + _lineSeries.Add(series); } - OnUpdate(args); - } -} -#endregion + /// + /// Called when indicator is initialized + /// + protected virtual void OnInit() + { + // Intentionally empty + } + + /// + /// Called on each update + /// + protected virtual void OnUpdate(UpdateArgs args) + { + // Intentionally empty + } + + /// + /// Called for chart painting + /// + public virtual void OnPaintChart(PaintChartEventArgs args) + { + // Intentionally empty + } + + /// + /// Initialize the indicator (for testing) + /// + public void Initialize() + { + OnInit(); + } + + /// + /// Process an update (for testing) + /// + public void ProcessUpdate(UpdateArgs args) + { + // Ensure line series have capacity for new data + foreach (var series in _lineSeries) + { + series.AddValue(); + } + OnUpdate(args); + } + } + + #endregion } diff --git a/quantower/Quantower.Tests.csproj b/quantower/Quantower.Tests.csproj index ae4c46f5..47a6f7e8 100644 --- a/quantower/Quantower.Tests.csproj +++ b/quantower/Quantower.Tests.csproj @@ -27,11 +27,11 @@ - + - + - + diff --git a/quantower/Trends.csproj b/quantower/Trends.csproj index 0a3b03d5..fd36fc89 100644 --- a/quantower/Trends.csproj +++ b/quantower/Trends.csproj @@ -16,8 +16,8 @@ - - + + ..\.github\TradingPlatform.BusinessLayer.dll