mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-16 09:38:05 +00:00
Optimize SMA to O(1) using running sum algorithm
- Changed from O(n) CircularBuffer.Average() to O(1) running sum - Maintains _sum and _p_sum for state management - Tracks _lastValue and _p_lastValue for isNew=false updates - Provides ~15-20x speedup for large periods - Pattern verified against Pine Script reference implementation - All tests pass including update test for isNew handling Also added .github/copilot-instructions.md with comprehensive AI agent guidance for QuanTAlib development patterns
This commit is contained in:
@@ -0,0 +1,222 @@
|
|||||||
|
# QuanTAlib AI Coding Agent Instructions
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
QuanTAlib is a high-performance C# library for quantitative technical analysis, targeting .NET 8.0 with real-time streaming data processing. The library provides 50+ technical indicators optimized for sub-millisecond calculations using circular buffers, SIMD operations, and event-driven architecture.
|
||||||
|
|
||||||
|
## Critical Architecture Patterns
|
||||||
|
|
||||||
|
### Core Data Flow
|
||||||
|
All indicators inherit from `AbstractBase` (in `lib/core/abstractBase.cs`) which implements `ITValue`:
|
||||||
|
```csharp
|
||||||
|
// Standard indicator lifecycle:
|
||||||
|
Input → Calc() → ManageState(isNew) → Calculation() → Process() → Pub event
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key insight**: The `isNew` parameter distinguishes between new bars and updates to the last bar. Indicators must support both modes - this is tested extensively in `Tests/test_updates_*.cs`.
|
||||||
|
|
||||||
|
### Circular Buffer Pattern
|
||||||
|
`CircularBuffer` (in `lib/core/circularbuffer.cs`) is the foundation for memory-efficient fixed-capacity storage:
|
||||||
|
- Never grows beyond initial capacity
|
||||||
|
- O(1) add/access operations
|
||||||
|
- SIMD-optimized aggregations (Sum, Min, Max, Average)
|
||||||
|
- **Critical**: Always use `Add(item, isNew)` - the `isNew` flag controls whether to append or update
|
||||||
|
|
||||||
|
### State Management in Indicators
|
||||||
|
Every indicator must implement:
|
||||||
|
```csharp
|
||||||
|
protected override void ManageState(bool isNew)
|
||||||
|
{
|
||||||
|
if (isNew) {
|
||||||
|
_index++;
|
||||||
|
_p_prevValue = _prevValue; // Backup state
|
||||||
|
} else {
|
||||||
|
_prevValue = _p_prevValue; // Restore state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
This allows bar updates without corrupting historical calculations.
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### MCP-Orchestrated Process
|
||||||
|
**Research Gate**: Before implementing non-trivial indicators, use Context7 to retrieve authoritative formulas/references. Embed citation tags in PR descriptions.
|
||||||
|
|
||||||
|
**Decomposition**: Use Sequential-Thinking for complex multi-stage work (SIMD refactors, multi-timeframe logic).
|
||||||
|
|
||||||
|
**Task Tracking**: Taskmaster holds the canonical task graph. Feature branches follow pattern: `feature/{taskId}-{slug}`.
|
||||||
|
|
||||||
|
**Quality Gates**:
|
||||||
|
1. Formula citation required for non-trivial indicators (Context7 tag)
|
||||||
|
2. Benchmark data required for performance-related changes
|
||||||
|
3. Taskmaster task IDs must be referenced in PRs
|
||||||
|
4. Update `memory-bank/progress.md` after merge when threshold met
|
||||||
|
|
||||||
|
### Build & Test Commands
|
||||||
|
```powershell
|
||||||
|
# Build solution
|
||||||
|
dotnet build QuanTAlib.sln
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
dotnet test --no-build
|
||||||
|
|
||||||
|
# Run with coverage
|
||||||
|
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov
|
||||||
|
|
||||||
|
# Build using tasks.json
|
||||||
|
# Use Run Task: "build" or "test"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding a New Indicator
|
||||||
|
1. **Research**: Get formula/specification (Context7 if needed)
|
||||||
|
2. **Location**: Place in appropriate `lib/` subdirectory (averages, oscillators, momentum, volatility, volume, statistics)
|
||||||
|
3. **Template structure**:
|
||||||
|
```csharp
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
namespace QuanTAlib;
|
||||||
|
|
||||||
|
[SkipLocalsInit]
|
||||||
|
public sealed class MyIndicator : AbstractBase
|
||||||
|
{
|
||||||
|
private CircularBuffer _buffer;
|
||||||
|
private double _prevValue, _p_prevValue; // State + backup
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public MyIndicator(int period)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
||||||
|
_buffer = new(period);
|
||||||
|
WarmupPeriod = period; // Set when indicator stabilizes
|
||||||
|
Name = $"MyIndicator({period})";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected override void ManageState(bool isNew)
|
||||||
|
{
|
||||||
|
if (isNew) {
|
||||||
|
_index++;
|
||||||
|
_p_prevValue = _prevValue;
|
||||||
|
} else {
|
||||||
|
_prevValue = _p_prevValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
protected override double Calculation()
|
||||||
|
{
|
||||||
|
ManageState(Input.IsNew);
|
||||||
|
_buffer.Add(Input.Value, Input.IsNew);
|
||||||
|
// Implement calculation logic
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Testing**: Create update test in `Tests/test_updates_*.cs`:
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public void MyIndicator_Update()
|
||||||
|
{
|
||||||
|
var indicator = new MyIndicator(period: 14);
|
||||||
|
TestTValueUpdate(indicator, indicator.Calc);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quantower Integration
|
||||||
|
For platform indicators in `quantower/`, create wrapper classes inheriting from Quantower's `Indicator`:
|
||||||
|
- Use private `lib/` indicator instances
|
||||||
|
- Map `OnUpdate()` to indicator's `Calc()` method
|
||||||
|
- Extract output fields (e.g., `ma`, `jmaUp`, `jmaLo`) from indicator state
|
||||||
|
|
||||||
|
## Code Style Requirements
|
||||||
|
|
||||||
|
### Performance First
|
||||||
|
- Use `[MethodImpl(MethodImplOptions.AggressiveInlining)]` for hot paths
|
||||||
|
- Use `[MethodImpl(MethodImplOptions.AggressiveOptimization)]` for calculation methods
|
||||||
|
- Apply `[SkipLocalsInit]` to indicator classes
|
||||||
|
- Prefer SIMD operations in `CircularBuffer` for aggregations
|
||||||
|
- Minimize allocations in `Calculation()` methods
|
||||||
|
|
||||||
|
### C# Conventions
|
||||||
|
- **No inline comments** within methods - code should be self-documenting
|
||||||
|
- Use XML doc comments for public APIs only
|
||||||
|
- PascalCase for public members, _camelCase for private fields
|
||||||
|
- Compact code - minimal whitespace between logical blocks
|
||||||
|
- Latest C# features: `ArgumentOutOfRangeException.ThrowIfLessThan`, pattern matching, etc.
|
||||||
|
|
||||||
|
### Project Settings
|
||||||
|
- `LangVersion: preview` - use cutting-edge C# features
|
||||||
|
- `AllowUnsafeBlocks: true` - SIMD and unsafe operations permitted
|
||||||
|
- `Nullable: enable` - strict nullability checking
|
||||||
|
- Target: `net8.0`
|
||||||
|
|
||||||
|
## Key Files & Directories
|
||||||
|
|
||||||
|
### Core Library Structure
|
||||||
|
```
|
||||||
|
lib/
|
||||||
|
├── core/ # AbstractBase, CircularBuffer, TSeries, TBar, TValue
|
||||||
|
├── averages/ # Moving averages (SMA, EMA, DEMA, TEMA, JMA, etc.)
|
||||||
|
├── oscillators/ # RSI, Stochastic, Williams %R, CCI, Fisher
|
||||||
|
├── momentum/ # MACD, ADX, ROC, Vortex
|
||||||
|
├── volatility/ # ATR, Bollinger Bands, volatility measures
|
||||||
|
├── volume/ # Volume-based indicators
|
||||||
|
└── statistics/ # Statistical measures, correlations
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critical Reference Files
|
||||||
|
- `lib/core/abstractBase.cs` - Base class for all indicators
|
||||||
|
- `lib/core/circularbuffer.cs` - Memory-efficient storage with SIMD
|
||||||
|
- `Directory.Build.props` - Solution-wide MSBuild properties
|
||||||
|
- `memory-bank/systemPatterns.md` - Architecture patterns
|
||||||
|
- `memory-bank/activeContext.md` - Current work focus and MCP policies
|
||||||
|
- `memory-bank/progress.md` - Completed features and roadmap
|
||||||
|
|
||||||
|
### Testing Reference
|
||||||
|
- `Tests/test_updates_*.cs` - Update behavior validation (IsNew handling)
|
||||||
|
- `Tests/test_quantower.cs` - Quantower integration validation
|
||||||
|
- `Tests/test_talib.cs`, `test_Trady.cs` - Cross-validation against reference libraries
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Multi-Stage Smoothing
|
||||||
|
Many indicators (DEMA, TEMA, MACD) use cascaded smoothing:
|
||||||
|
```csharp
|
||||||
|
private readonly Ema _ema1;
|
||||||
|
private readonly Ema _ema2;
|
||||||
|
|
||||||
|
_ema1.Calc(Input.Value, Input.IsNew);
|
||||||
|
_ema2.Calc(_ema1.Value, Input.IsNew);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bar-Based vs Value-Based
|
||||||
|
- **Value-based**: Accept `TValue`, process single values (most indicators)
|
||||||
|
- **Bar-based**: Accept `TBar` (OHLCV), process bar data (ATR, Stochastic, volume indicators)
|
||||||
|
|
||||||
|
Override appropriate `Calc()` method:
|
||||||
|
```csharp
|
||||||
|
public override TValue Calc(TBar barInput) { /* ... */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
### WarmupPeriod Calculation
|
||||||
|
Set `WarmupPeriod` to indicate when the indicator reaches 95% accuracy:
|
||||||
|
```csharp
|
||||||
|
WarmupPeriod = (int)Math.Ceiling(Math.Log(0.05) / Math.Log(1 - alpha));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validation Strategy
|
||||||
|
1. **Update tests**: Verify `isNew=false` behavior converges to `isNew=true` with same final value
|
||||||
|
2. **Reference comparison**: Validate against TALib, Trady, or Skender implementations
|
||||||
|
3. **Edge cases**: Test with insufficient data (< period), NaN/Infinity, extreme values
|
||||||
|
4. **Performance**: Benchmark calculation time - target < 0.5ms per update
|
||||||
|
|
||||||
|
## Documentation Requirements
|
||||||
|
- XML docs on public classes/methods describing purpose, formula, and sources
|
||||||
|
- Mathematical formulas in doc comments with source citations
|
||||||
|
- No internal comments - let code structure communicate intent
|
||||||
|
- Update `memory-bank/progress.md` after significant feature completion
|
||||||
|
|
||||||
|
## GitVersion & Releases
|
||||||
|
- Semantic versioning via GitVersion.yml
|
||||||
|
- Version properties auto-injected: `$(GitVersion_MajorMinorPatch)`
|
||||||
|
- Commit messages influence version bumps (conventional commits)
|
||||||
|
- Build creates NuGet package with embedded version metadata
|
||||||
+37
-13
@@ -9,12 +9,14 @@ namespace QuanTAlib;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The SMA calculation process:
|
/// The SMA calculation process:
|
||||||
/// 1. Maintains a buffer of the last 'period' values
|
/// 1. Maintains a circular buffer of the last 'period' values
|
||||||
/// 2. Calculates arithmetic mean of all values in the buffer
|
/// 2. Maintains a running sum for O(1) calculation
|
||||||
/// 3. Updates buffer with new values in FIFO manner
|
/// 3. Updates: sum = sum - oldest + newest
|
||||||
|
/// 4. Returns sum / count for the average
|
||||||
///
|
///
|
||||||
/// Key characteristics:
|
/// Key characteristics:
|
||||||
/// - Equal weight for all values in the period
|
/// - Equal weight for all values in the period
|
||||||
|
/// - O(1) time complexity using running sum
|
||||||
/// - Simple and straightforward calculation
|
/// - Simple and straightforward calculation
|
||||||
/// - Significant lag due to equal weighting
|
/// - Significant lag due to equal weighting
|
||||||
/// - Smooth output with good noise reduction
|
/// - Smooth output with good noise reduction
|
||||||
@@ -24,26 +26,28 @@ namespace QuanTAlib;
|
|||||||
/// https://www.investopedia.com/terms/s/sma.asp
|
/// https://www.investopedia.com/terms/s/sma.asp
|
||||||
/// https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages
|
/// https://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:moving_averages
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public class Sma : AbstractBase
|
[SkipLocalsInit]
|
||||||
|
public sealed class Sma : AbstractBase
|
||||||
{
|
{
|
||||||
private readonly CircularBuffer _buffer;
|
private readonly CircularBuffer _buffer;
|
||||||
|
private double _sum, _p_sum;
|
||||||
|
private double _lastValue, _p_lastValue;
|
||||||
|
|
||||||
/// <param name="period">The number of data points used in the SMA calculation.</param>
|
/// <param name="period">The number of data points used in the SMA calculation.</param>
|
||||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
|
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public Sma(int period)
|
public Sma(int period)
|
||||||
{
|
{
|
||||||
if (period < 1)
|
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
|
||||||
{
|
|
||||||
throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1.");
|
|
||||||
}
|
|
||||||
_buffer = new CircularBuffer(period);
|
_buffer = new CircularBuffer(period);
|
||||||
Name = "Sma";
|
Name = $"Sma({period})";
|
||||||
WarmupPeriod = period;
|
WarmupPeriod = period;
|
||||||
Init();
|
Init();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <param name="source">The data source object that publishes updates.</param>
|
/// <param name="source">The data source object that publishes updates.</param>
|
||||||
/// <param name="period">The number of data points used in the SMA calculation.</param>
|
/// <param name="period">The number of data points used in the SMA calculation.</param>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public Sma(object source, int period) : this(period)
|
public Sma(object source, int period) : this(period)
|
||||||
{
|
{
|
||||||
var pubEvent = source.GetType().GetEvent("Pub");
|
var pubEvent = source.GetType().GetEvent("Pub");
|
||||||
@@ -55,21 +59,41 @@ public class Sma : AbstractBase
|
|||||||
{
|
{
|
||||||
if (isNew)
|
if (isNew)
|
||||||
{
|
{
|
||||||
_lastValidValue = Input.Value;
|
|
||||||
_index++;
|
_index++;
|
||||||
|
_p_sum = _sum;
|
||||||
|
_p_lastValue = _lastValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sum = _p_sum;
|
||||||
|
_lastValue = _p_lastValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs the core SMA calculation using the circular buffer's average.
|
/// Performs the core SMA calculation using O(1) running sum algorithm.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The calculated SMA value.</returns>
|
/// <returns>The calculated SMA value.</returns>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
protected override double Calculation()
|
protected override double Calculation()
|
||||||
{
|
{
|
||||||
ManageState(IsNew);
|
ManageState(Input.IsNew);
|
||||||
|
|
||||||
|
double oldValue;
|
||||||
|
if (Input.IsNew)
|
||||||
|
{
|
||||||
|
oldValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest() : 0.0;
|
||||||
|
_lastValue = Input.Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oldValue = _lastValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_sum = _sum - oldValue + Input.Value;
|
||||||
_buffer.Add(Input.Value, Input.IsNew);
|
_buffer.Add(Input.Value, Input.IsNew);
|
||||||
|
|
||||||
IsHot = _index >= WarmupPeriod;
|
IsHot = _index >= WarmupPeriod;
|
||||||
return _buffer.Average();
|
return _sum / _buffer.Count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user