mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-03 03:47:42 +00:00
updates from mac
This commit is contained in:
+12
-12
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"gitversion.tool": {
|
||||
"version": "5.12.0",
|
||||
"commands": [
|
||||
"dotnet-gitversion"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"gitversion.tool": {
|
||||
"version": "5.12.0",
|
||||
"commands": [
|
||||
"dotnet-gitversion"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,17 +1,17 @@
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "csharp"
|
||||
enabled = true
|
||||
|
||||
[[analyzers]]
|
||||
name = "test-coverage"
|
||||
enabled = true
|
||||
|
||||
[[analyzers]]
|
||||
name = "secrets"
|
||||
enabled = true
|
||||
|
||||
[[transformers]]
|
||||
name = "dotnet-format"
|
||||
version = 1
|
||||
|
||||
[[analyzers]]
|
||||
name = "csharp"
|
||||
enabled = true
|
||||
|
||||
[[analyzers]]
|
||||
name = "test-coverage"
|
||||
enabled = true
|
||||
|
||||
[[analyzers]]
|
||||
name = "secrets"
|
||||
enabled = true
|
||||
|
||||
[[transformers]]
|
||||
name = "dotnet-format"
|
||||
enabled = true
|
||||
+6585
-6585
File diff suppressed because it is too large
Load Diff
@@ -1,51 +1,51 @@
|
||||
*Act:** meticulous auto-agent; finish fully—no early stop.
|
||||
Cycle: State→Do→Verify→Iterate→Validate.
|
||||
Style: concise, exact, verifiable.
|
||||
|
||||
### 🧰 Tool Roles
|
||||
| Tool | Purpose |
|
||||
|------|----------|
|
||||
| **seq-think-mcp** | plan & decompose tasks |
|
||||
| **tavily-mcp** | fresh info, web/news search |
|
||||
| **ref-tools-mcp** | lib/framework specs |
|
||||
| **wolfram-mcp** | math/logic/symbolic verify |
|
||||
| **git-mcp** | code/docs search, lint, commit rules |
|
||||
| **qdrant-mcp** | long-term memory (no secrets) |
|
||||
|
||||
Fallback: search→tavily→ref→git | plan→seq | calc→wolfram | persist→qdrant.
|
||||
|
||||
### ⚙️ Workflow
|
||||
1️⃣ **Recall/Discover:** qdrant for mem, tavily/ref/wolfram for current info.
|
||||
2️⃣ **Analyze:** define expected, edges, deps, pitfalls; plan via seq-think.
|
||||
3️⃣ **Investigate:** git search→read context→root cause; log in qdrant.
|
||||
4️⃣ **Research:** tavily search→extract; ref-tools for stds; git for docs; recurse; save refs.
|
||||
5️⃣ **Plan:** seq-think build TODO (emoji status); store in qdrant.
|
||||
6️⃣ **Implement:** small testable edits; read ≤2k lines; make `.env` if missing.
|
||||
7️⃣ **Debug:** logs/probes; fix root; reverify each step.
|
||||
8️⃣ **Test:** run per change; add edges; repeat till pass.
|
||||
9️⃣ **Validate:** confirm intent; hidden tests; math check via wolfram.
|
||||
🔟 **Memorize:** store verified facts `{text,meta:{src,proj,date,tags}}` → qdrant; tag old deprecated.
|
||||
|
||||
### 💬 Comm
|
||||
Speak clear, brief, pro-casual.
|
||||
Use bullets/code; no filler.
|
||||
Write direct to files; show only if asked.
|
||||
|
||||
### 🪶 Git Policy
|
||||
No auto-commit—only on user cmd.
|
||||
Before commit:
|
||||
1) verify scope/tests ✔
|
||||
2) check rules via git + qdrant ✔
|
||||
3) `git add` → `git commit -m "<msg>"`
|
||||
Msg: subj ≤50ch, imperative; body ≤72ch what/why; footer refs/trailers.
|
||||
Checklist: concise ✔ why ✔ refs ✔ style ✔ tests ✔
|
||||
|
||||
### ⚠️ Error Handling
|
||||
If unclear → reverify (tavily/ref).
|
||||
Math gap → wolfram.
|
||||
Missing ctx → qdrant.
|
||||
Multi-path → seq-think fork.
|
||||
|
||||
### ✅ Goal
|
||||
Deliver complete, tested, verified soln; persist in qdrant.
|
||||
Loop: Plan→Exec→Verify→Persist→Confirm.
|
||||
*Act:** meticulous auto-agent; finish fully—no early stop.
|
||||
Cycle: State→Do→Verify→Iterate→Validate.
|
||||
Style: concise, exact, verifiable.
|
||||
|
||||
### 🧰 Tool Roles
|
||||
| Tool | Purpose |
|
||||
|------|----------|
|
||||
| **seq-think-mcp** | plan & decompose tasks |
|
||||
| **tavily-mcp** | fresh info, web/news search |
|
||||
| **ref-tools-mcp** | lib/framework specs |
|
||||
| **wolfram-mcp** | math/logic/symbolic verify |
|
||||
| **git-mcp** | code/docs search, lint, commit rules |
|
||||
| **qdrant-mcp** | long-term memory (no secrets) |
|
||||
|
||||
Fallback: search→tavily→ref→git | plan→seq | calc→wolfram | persist→qdrant.
|
||||
|
||||
### ⚙️ Workflow
|
||||
1️⃣ **Recall/Discover:** qdrant for mem, tavily/ref/wolfram for current info.
|
||||
2️⃣ **Analyze:** define expected, edges, deps, pitfalls; plan via seq-think.
|
||||
3️⃣ **Investigate:** git search→read context→root cause; log in qdrant.
|
||||
4️⃣ **Research:** tavily search→extract; ref-tools for stds; git for docs; recurse; save refs.
|
||||
5️⃣ **Plan:** seq-think build TODO (emoji status); store in qdrant.
|
||||
6️⃣ **Implement:** small testable edits; read ≤2k lines; make `.env` if missing.
|
||||
7️⃣ **Debug:** logs/probes; fix root; reverify each step.
|
||||
8️⃣ **Test:** run per change; add edges; repeat till pass.
|
||||
9️⃣ **Validate:** confirm intent; hidden tests; math check via wolfram.
|
||||
🔟 **Memorize:** store verified facts `{text,meta:{src,proj,date,tags}}` → qdrant; tag old deprecated.
|
||||
|
||||
### 💬 Comm
|
||||
Speak clear, brief, pro-casual.
|
||||
Use bullets/code; no filler.
|
||||
Write direct to files; show only if asked.
|
||||
|
||||
### 🪶 Git Policy
|
||||
No auto-commit—only on user cmd.
|
||||
Before commit:
|
||||
1) verify scope/tests ✔
|
||||
2) check rules via git + qdrant ✔
|
||||
3) `git add` → `git commit -m "<msg>"`
|
||||
Msg: subj ≤50ch, imperative; body ≤72ch what/why; footer refs/trailers.
|
||||
Checklist: concise ✔ why ✔ refs ✔ style ✔ tests ✔
|
||||
|
||||
### ⚠️ Error Handling
|
||||
If unclear → reverify (tavily/ref).
|
||||
Math gap → wolfram.
|
||||
Missing ctx → qdrant.
|
||||
Multi-path → seq-think fork.
|
||||
|
||||
### ✅ Goal
|
||||
Deliver complete, tested, verified soln; persist in qdrant.
|
||||
Loop: Plan→Exec→Verify→Persist→Confirm.
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
name: "CodeQL Config"
|
||||
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
- uses: security-extended
|
||||
|
||||
paths-ignore:
|
||||
- '**/test/**'
|
||||
- '**/tests/**'
|
||||
- '**/*.test.cs'
|
||||
- '**/obj/**'
|
||||
- '**/bin/**'
|
||||
- '**/docs/**'
|
||||
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- warning
|
||||
- recommendation
|
||||
|
||||
paths:
|
||||
name: "CodeQL Config"
|
||||
|
||||
queries:
|
||||
- uses: security-and-quality
|
||||
- uses: security-extended
|
||||
|
||||
paths-ignore:
|
||||
- '**/test/**'
|
||||
- '**/tests/**'
|
||||
- '**/*.test.cs'
|
||||
- '**/obj/**'
|
||||
- '**/bin/**'
|
||||
- '**/docs/**'
|
||||
|
||||
query-filters:
|
||||
- exclude:
|
||||
problem.severity:
|
||||
- warning
|
||||
- recommendation
|
||||
|
||||
paths:
|
||||
- src
|
||||
@@ -1,222 +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
|
||||
# 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
|
||||
|
||||
+342
-342
@@ -1,342 +1,342 @@
|
||||
name: Publish Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.gitignore'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.gitignore'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read # Allows SonarCloud to decorate PRs with analysis results
|
||||
security-events: write # Required for CodeQL analysis and uploading SARIF results
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '8.x'
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
Code_Coverage:
|
||||
timeout-minutes: 30
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: ${{ runner.os }}-nuget-
|
||||
|
||||
- name: Cache dotnet tools
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.dotnet/tools
|
||||
key: ${{ runner.os }}-dotnet-tools-${{ hashFiles('**/*.csproj') }}
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install dotnet tools
|
||||
run: |
|
||||
dotnet tool install JetBrains.dotCover.GlobalTool --global
|
||||
dotnet tool install dotnet-sonarscanner --global
|
||||
dotnet tool install dotnet-coverage --global
|
||||
dotnet tool install --global coverlet.console
|
||||
dotnet tool install --global dotnet-reportgenerator-globaltool
|
||||
dotnet restore
|
||||
|
||||
- name: Begin SonarCloud Analysis
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
dotnet sonarscanner begin /k:"mihakralj_QuanTAlib" /o:"mihakralj-quantalib" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" `
|
||||
/d:sonar.solution.file="QuanTAlib.sln" `
|
||||
/d:sonar.cs.opencover.reportsPaths="**/*cover*.xml" `
|
||||
/d:sonar.cs.dotcover.reportsPaths="**/dotcover.xml" `
|
||||
/d:sonar.coverage.exclusions="**Tests.cs,**/*.md,**/*.html,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*,**/obj/**/*,**/bin/**/*" `
|
||||
/d:sonar.exclusions="**/TestResults/**/*,**/bin/**/*,**/obj/**/*,**/*.html,**/coverage/**/*,**/CoverageReport/**/*,**/*.md,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*" `
|
||||
/d:sonar.test.exclusions="**Tests.cs,**/obj/**/*,**/bin/**/*" `
|
||||
/d:sonar.cpd.exclusions="**Tests.cs" `
|
||||
/d:sonar.scanner.scanAll="false" `
|
||||
/d:sonar.cs.roslyn.ignoreIssues="false" `
|
||||
/d:sonar.issue.ignore.multicriteria="e1" `
|
||||
/d:sonar.issue.ignore.multicriteria.e1.ruleKey="csharpsquid:S1944,csharpsquid:S2053,csharpsquid:S2222,csharpsquid:S2259,csharpsquid:S2583,csharpsquid:S2589,csharpsquid:S3329,csharpsquid:S3655,csharpsquid:S3900,csharpsquid:S3949,csharpsquid:S3966,csharpsquid:S4158,csharpsquid:S4347,csharpsquid:S5773,csharpsquid:S6781" `
|
||||
/d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/*.cs" `
|
||||
/d:sonar.verbose="true"
|
||||
|
||||
- name: Build Projects
|
||||
id: build
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet build --no-restore --configuration Debug
|
||||
dotnet build ./lib/quantalib.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Averages/_Averages.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Statistics/_Statistics.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Volatility/_Volatility.csproj --configuration Release --nologo
|
||||
dotnet build ./SyntheticVendor/SyntheticVendor.csproj --configuration Release --nologo
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Build failed" }
|
||||
|
||||
- name: Check Build Status
|
||||
if: steps.build.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
- name: Run Tests with Coverage
|
||||
id: tests
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet test --no-build --configuration Debug /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
dotnet-coverage collect "dotnet test" -f xml -o "coverage.xml"
|
||||
dotnet dotcover test Tests/Tests.csproj --dcReportType=HTML --dcoutput=./dotcover.html
|
||||
dotnet dotcover test Tests/Tests.csproj --dcReportType=DetailedXML --dcoutput=./dotcover.xml --verbosity=Detailed
|
||||
dotnet test -p:CollectCoverage=true --collect:"XPlat Code Coverage" --results-directory "./"
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: |
|
||||
reportgenerator -reports:*cover*.xml -targetdir:./coverage-report
|
||||
|
||||
- name: Upload Coverage Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
**/TestResults
|
||||
**/coverage-report
|
||||
**/*cover*.xml
|
||||
**/dotcover.*
|
||||
|
||||
- name: End SonarCloud Analysis
|
||||
if: always()
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
- name: Upload Coverage to Codacy
|
||||
uses: codacy/codacy-coverage-reporter-action@v1
|
||||
with:
|
||||
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
coverage-reports: '*cover*.xml'
|
||||
|
||||
- name: Upload Coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: 'cover*'
|
||||
verbose: true
|
||||
|
||||
CodeQL:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: ${{ runner.os }}-nuget-
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: 'csharp'
|
||||
queries: security-and-quality
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
tools: linked
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --no-restore --configuration Debug
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
output: results
|
||||
upload: true
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/dotnet@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
LD_PRELOAD: '' # Clear the LD_PRELOAD to avoid CodeQL conflicts
|
||||
with:
|
||||
args: |
|
||||
--file=./lib/quantalib.csproj
|
||||
--severity-threshold=low
|
||||
--detection-depth=4
|
||||
--package-manager=nuget
|
||||
|
||||
- name: Run Snyk on Solution
|
||||
uses: snyk/actions/dotnet@master
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
LD_PRELOAD: ''
|
||||
with:
|
||||
args: |
|
||||
--file=QuanTAlib.sln
|
||||
--all-projects
|
||||
--detection-depth=4
|
||||
|
||||
- name: Run Snyk IaC
|
||||
uses: snyk/actions/iac@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
LD_PRELOAD: ''
|
||||
with:
|
||||
args: |
|
||||
--severity-threshold=low
|
||||
|
||||
build_publish:
|
||||
timeout-minutes: 20
|
||||
needs: [Code_Coverage, CodeQL]
|
||||
if: |
|
||||
success() &&
|
||||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Install GitVersion
|
||||
uses: gittools/actions/gitversion/setup@v0
|
||||
with:
|
||||
versionSpec: '6.x'
|
||||
includePrerelease: true
|
||||
|
||||
- name: Determine Version
|
||||
id: gitversion
|
||||
uses: gittools/actions/gitversion/execute@v0
|
||||
with:
|
||||
useConfigFile: true
|
||||
updateAssemblyInfo: false
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: ${{ runner.os }}-nuget-
|
||||
|
||||
- name: Build projects
|
||||
run: |
|
||||
dotnet restore
|
||||
dotnet build ./lib/quantalib.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Averages/_Averages.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Statistics/_Statistics.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Volatility/_Volatility.csproj --configuration Release --nologo
|
||||
dotnet build ./SyntheticVendor/SyntheticVendor.csproj --configuration Release --nologo
|
||||
|
||||
- name: Create or Update Development Release
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release delete development --yes || true
|
||||
gh release create development \
|
||||
--title "Development Build" \
|
||||
--notes "Latest development build from commit ${{ github.sha }}" \
|
||||
--prerelease \
|
||||
--target ${{ github.sha }} \
|
||||
lib/bin/Release/QuanTAlib.dll \
|
||||
quantower/Averages/bin/Release/Averages.dll \
|
||||
quantower/Statistics/bin/Release/Statistics.dll \
|
||||
quantower/Volatility/bin/Release/Volatility.dll \
|
||||
SyntheticVendor/bin/Release/SyntheticVendor.dll
|
||||
|
||||
- name: Push prerelease package to myget.org
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
continue-on-error: true
|
||||
id: myget-push
|
||||
run: |
|
||||
dotnet nuget push 'lib/bin/Release/QuanTAlib.*.nupkg' \
|
||||
--source https://www.myget.org/F/quantalib/api/v3/index.json \
|
||||
--force-english-output \
|
||||
--api-key ${{ secrets.MYGET_DEPLOY_KEY_QUANTALIB }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create v${{ steps.gitversion.outputs.MajorMinorPatch }} \
|
||||
--title "Release from commit ${{ steps.gitversion.outputs.MajorMinorPatch }}" \
|
||||
--notes "Release notes for this version." \
|
||||
quantower/Averages/bin/Release/Averages.dll \
|
||||
quantower/Statistics/bin/Release/Statistics.dll \
|
||||
quantower/Volatility/bin/Release/Volatility.dll \
|
||||
SyntheticVendor/bin/Release/SyntheticVendor.dll
|
||||
|
||||
- name: Push release package to nuget.org
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
dotnet nuget push 'lib/bin/Release/QuanTAlib.*.nupkg' \
|
||||
--source https://api.nuget.org/v3/index.json \
|
||||
--skip-duplicate \
|
||||
--api-key ${{ secrets.NUGET_DEPLOY_KEY_QUANTLIB }}
|
||||
name: Publish Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.gitignore'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- 'docs/**'
|
||||
- '.gitignore'
|
||||
- 'LICENSE'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read # Allows SonarCloud to decorate PRs with analysis results
|
||||
security-events: write # Required for CodeQL analysis and uploading SARIF results
|
||||
|
||||
env:
|
||||
DOTNET_VERSION: '8.x'
|
||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
|
||||
DOTNET_CLI_TELEMETRY_OPTOUT: true
|
||||
|
||||
jobs:
|
||||
Code_Coverage:
|
||||
timeout-minutes: 30
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: ${{ runner.os }}-nuget-
|
||||
|
||||
- name: Cache dotnet tools
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.dotnet/tools
|
||||
key: ${{ runner.os }}-dotnet-tools-${{ hashFiles('**/*.csproj') }}
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: 'zulu'
|
||||
|
||||
- name: Cache SonarCloud scanner
|
||||
id: cache-sonar-scanner
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: .\.sonar\scanner
|
||||
key: ${{ runner.os }}-sonar-scanner
|
||||
restore-keys: ${{ runner.os }}-sonar-scanner
|
||||
|
||||
- name: Install dotnet tools
|
||||
run: |
|
||||
dotnet tool install JetBrains.dotCover.GlobalTool --global
|
||||
dotnet tool install dotnet-sonarscanner --global
|
||||
dotnet tool install dotnet-coverage --global
|
||||
dotnet tool install --global coverlet.console
|
||||
dotnet tool install --global dotnet-reportgenerator-globaltool
|
||||
dotnet restore
|
||||
|
||||
- name: Begin SonarCloud Analysis
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: |
|
||||
dotnet sonarscanner begin /k:"mihakralj_QuanTAlib" /o:"mihakralj-quantalib" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" `
|
||||
/d:sonar.solution.file="QuanTAlib.sln" `
|
||||
/d:sonar.cs.opencover.reportsPaths="**/*cover*.xml" `
|
||||
/d:sonar.cs.dotcover.reportsPaths="**/dotcover.xml" `
|
||||
/d:sonar.coverage.exclusions="**Tests.cs,**/*.md,**/*.html,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*,**/obj/**/*,**/bin/**/*" `
|
||||
/d:sonar.exclusions="**/TestResults/**/*,**/bin/**/*,**/obj/**/*,**/*.html,**/coverage/**/*,**/CoverageReport/**/*,**/*.md,**/*.css,**/docs/**/*,**/archive/**/*,**/notebooks/**/*" `
|
||||
/d:sonar.test.exclusions="**Tests.cs,**/obj/**/*,**/bin/**/*" `
|
||||
/d:sonar.cpd.exclusions="**Tests.cs" `
|
||||
/d:sonar.scanner.scanAll="false" `
|
||||
/d:sonar.cs.roslyn.ignoreIssues="false" `
|
||||
/d:sonar.issue.ignore.multicriteria="e1" `
|
||||
/d:sonar.issue.ignore.multicriteria.e1.ruleKey="csharpsquid:S1944,csharpsquid:S2053,csharpsquid:S2222,csharpsquid:S2259,csharpsquid:S2583,csharpsquid:S2589,csharpsquid:S3329,csharpsquid:S3655,csharpsquid:S3900,csharpsquid:S3949,csharpsquid:S3966,csharpsquid:S4158,csharpsquid:S4347,csharpsquid:S5773,csharpsquid:S6781" `
|
||||
/d:sonar.issue.ignore.multicriteria.e1.resourceKey="**/*.cs" `
|
||||
/d:sonar.verbose="true"
|
||||
|
||||
- name: Build Projects
|
||||
id: build
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet build --no-restore --configuration Debug
|
||||
dotnet build ./lib/quantalib.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Averages/_Averages.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Statistics/_Statistics.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Volatility/_Volatility.csproj --configuration Release --nologo
|
||||
dotnet build ./SyntheticVendor/SyntheticVendor.csproj --configuration Release --nologo
|
||||
if ($LASTEXITCODE -ne 0) { Write-Error "Build failed" }
|
||||
|
||||
- name: Check Build Status
|
||||
if: steps.build.outcome == 'failure'
|
||||
run: exit 1
|
||||
|
||||
- name: Run Tests with Coverage
|
||||
id: tests
|
||||
continue-on-error: true
|
||||
run: |
|
||||
dotnet test --no-build --configuration Debug /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
|
||||
dotnet-coverage collect "dotnet test" -f xml -o "coverage.xml"
|
||||
dotnet dotcover test Tests/Tests.csproj --dcReportType=HTML --dcoutput=./dotcover.html
|
||||
dotnet dotcover test Tests/Tests.csproj --dcReportType=DetailedXML --dcoutput=./dotcover.xml --verbosity=Detailed
|
||||
dotnet test -p:CollectCoverage=true --collect:"XPlat Code Coverage" --results-directory "./"
|
||||
|
||||
- name: Generate Coverage Report
|
||||
run: |
|
||||
reportgenerator -reports:*cover*.xml -targetdir:./coverage-report
|
||||
|
||||
- name: Upload Coverage Reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-reports
|
||||
path: |
|
||||
**/TestResults
|
||||
**/coverage-report
|
||||
**/*cover*.xml
|
||||
**/dotcover.*
|
||||
|
||||
- name: End SonarCloud Analysis
|
||||
if: always()
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
shell: powershell
|
||||
run: dotnet sonarscanner end /d:sonar.token="${{ secrets.SONAR_TOKEN }}"
|
||||
|
||||
- name: Upload Coverage to Codacy
|
||||
uses: codacy/codacy-coverage-reporter-action@v1
|
||||
with:
|
||||
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
|
||||
coverage-reports: '*cover*.xml'
|
||||
|
||||
- name: Upload Coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: 'cover*'
|
||||
verbose: true
|
||||
|
||||
CodeQL:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: ${{ runner.os }}-nuget-
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: 'csharp'
|
||||
queries: security-and-quality
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
tools: linked
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --no-restore --configuration Debug
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
output: results
|
||||
upload: true
|
||||
|
||||
- name: Run Snyk to check for vulnerabilities
|
||||
uses: snyk/actions/dotnet@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
LD_PRELOAD: '' # Clear the LD_PRELOAD to avoid CodeQL conflicts
|
||||
with:
|
||||
args: |
|
||||
--file=./lib/quantalib.csproj
|
||||
--severity-threshold=low
|
||||
--detection-depth=4
|
||||
--package-manager=nuget
|
||||
|
||||
- name: Run Snyk on Solution
|
||||
uses: snyk/actions/dotnet@master
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
LD_PRELOAD: ''
|
||||
with:
|
||||
args: |
|
||||
--file=QuanTAlib.sln
|
||||
--all-projects
|
||||
--detection-depth=4
|
||||
|
||||
- name: Run Snyk IaC
|
||||
uses: snyk/actions/iac@master
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
LD_PRELOAD: ''
|
||||
with:
|
||||
args: |
|
||||
--severity-threshold=low
|
||||
|
||||
build_publish:
|
||||
timeout-minutes: 20
|
||||
needs: [Code_Coverage, CodeQL]
|
||||
if: |
|
||||
success() &&
|
||||
(github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup .NET SDK
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: ${{ env.DOTNET_VERSION }}
|
||||
|
||||
- name: Install GitVersion
|
||||
uses: gittools/actions/gitversion/setup@v0
|
||||
with:
|
||||
versionSpec: '6.x'
|
||||
includePrerelease: true
|
||||
|
||||
- name: Determine Version
|
||||
id: gitversion
|
||||
uses: gittools/actions/gitversion/execute@v0
|
||||
with:
|
||||
useConfigFile: true
|
||||
updateAssemblyInfo: false
|
||||
|
||||
- name: Cache NuGet packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
|
||||
restore-keys: ${{ runner.os }}-nuget-
|
||||
|
||||
- name: Build projects
|
||||
run: |
|
||||
dotnet restore
|
||||
dotnet build ./lib/quantalib.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Averages/_Averages.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Statistics/_Statistics.csproj --configuration Release --nologo
|
||||
dotnet build ./quantower/Volatility/_Volatility.csproj --configuration Release --nologo
|
||||
dotnet build ./SyntheticVendor/SyntheticVendor.csproj --configuration Release --nologo
|
||||
|
||||
- name: Create or Update Development Release
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release delete development --yes || true
|
||||
gh release create development \
|
||||
--title "Development Build" \
|
||||
--notes "Latest development build from commit ${{ github.sha }}" \
|
||||
--prerelease \
|
||||
--target ${{ github.sha }} \
|
||||
lib/bin/Release/QuanTAlib.dll \
|
||||
quantower/Averages/bin/Release/Averages.dll \
|
||||
quantower/Statistics/bin/Release/Statistics.dll \
|
||||
quantower/Volatility/bin/Release/Volatility.dll \
|
||||
SyntheticVendor/bin/Release/SyntheticVendor.dll
|
||||
|
||||
- name: Push prerelease package to myget.org
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
continue-on-error: true
|
||||
id: myget-push
|
||||
run: |
|
||||
dotnet nuget push 'lib/bin/Release/QuanTAlib.*.nupkg' \
|
||||
--source https://www.myget.org/F/quantalib/api/v3/index.json \
|
||||
--force-english-output \
|
||||
--api-key ${{ secrets.MYGET_DEPLOY_KEY_QUANTALIB }}
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/main'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release create v${{ steps.gitversion.outputs.MajorMinorPatch }} \
|
||||
--title "Release from commit ${{ steps.gitversion.outputs.MajorMinorPatch }}" \
|
||||
--notes "Release notes for this version." \
|
||||
quantower/Averages/bin/Release/Averages.dll \
|
||||
quantower/Statistics/bin/Release/Statistics.dll \
|
||||
quantower/Volatility/bin/Release/Volatility.dll \
|
||||
SyntheticVendor/bin/Release/SyntheticVendor.dll
|
||||
|
||||
- name: Push release package to nuget.org
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
dotnet nuget push 'lib/bin/Release/QuanTAlib.*.nupkg' \
|
||||
--source https://api.nuget.org/v3/index.json \
|
||||
--skip-duplicate \
|
||||
--api-key ${{ secrets.NUGET_DEPLOY_KEY_QUANTLIB }}
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"sonarCloudOrganization": "mihakralj-quantalib",
|
||||
"projectKey": "mihakralj_QuanTAlib"
|
||||
"projectKey": "mihakralj_QuanTAlib",
|
||||
"region": "EU"
|
||||
}
|
||||
Vendored
+7
-7
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-dotnettools.csharp",
|
||||
"bierner.markdown-mermaid"
|
||||
]
|
||||
}
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csdevkit",
|
||||
"ms-dotnettools.csharp",
|
||||
"bierner.markdown-mermaid"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+191
-190
@@ -1,190 +1,191 @@
|
||||
{
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// GitHub Copilot Settings for QuanTAlib Workspace
|
||||
// Optimized for high-performance financial library development
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Copilot Core Settings
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Enable Copilot completions (suggestions appear automatically)
|
||||
"github.copilot.editor.enableAutoCompletions": true,
|
||||
|
||||
// Enable Copilot for all file types
|
||||
"github.copilot.enable": {
|
||||
"*": true,
|
||||
"plaintext": false,
|
||||
"markdown": true,
|
||||
"scminput": false
|
||||
},
|
||||
|
||||
// Show inline suggestions
|
||||
"editor.inlineSuggest.enabled": true,
|
||||
|
||||
// Always show the inline suggestion toolbar
|
||||
"editor.inlineSuggest.showToolbar": "always",
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Copilot Chat Settings (Manual Review Required)
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// DO NOT auto-apply chat edits - require manual review for quality control
|
||||
"chat.editing.autoApply": "off",
|
||||
|
||||
// Confirm before removing edit requests
|
||||
"chat.editing.confirmEditRequestRemoval": true,
|
||||
|
||||
// Show chat panel on the side
|
||||
"chat.editor.wordWrap": "on",
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Editor Settings for Productivity
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Enable quick suggestions in all contexts
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": true,
|
||||
"strings": true
|
||||
},
|
||||
|
||||
// Show suggestions on trigger characters
|
||||
"editor.suggestOnTriggerCharacters": true,
|
||||
|
||||
// Accept suggestion on commit character (like dot, parenthesis)
|
||||
"editor.acceptSuggestionOnCommitCharacter": true,
|
||||
|
||||
// Faster suggestion appearance
|
||||
"editor.quickSuggestionsDelay": 0,
|
||||
|
||||
// Show snippet suggestions with other suggestions
|
||||
"editor.snippetSuggestions": "inline",
|
||||
|
||||
// Tab key behavior
|
||||
"editor.tabCompletion": "on",
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// C# Specific Settings
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"[csharp]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": true,
|
||||
"strings": true
|
||||
}
|
||||
},
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Performance & Quality Control
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Save automatically (helps with Copilot context)
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.autoSaveDelay": 1000,
|
||||
|
||||
// Show whitespace (important for performance-critical code)
|
||||
"editor.renderWhitespace": "boundary",
|
||||
|
||||
// Show inline parameter hints
|
||||
"editor.inlayHints.enabled": "on",
|
||||
|
||||
// Highlight matching brackets
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Git Integration
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Auto-fetch git changes
|
||||
"git.autofetch": true,
|
||||
|
||||
// Confirm before synchronizing
|
||||
"git.confirmSync": false,
|
||||
|
||||
// Show inline blame
|
||||
"git.decorations.enabled": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Terminal Settings (Preserved from original)
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"terminal.integrated.defaultProfile.windows": "PowerShell",
|
||||
"terminal.integrated.profiles.windows": {
|
||||
"PowerShell": {
|
||||
"source": "PowerShell",
|
||||
"icon": "terminal-powershell"
|
||||
}
|
||||
},
|
||||
"terminal.integrated.shellIntegration.enabled": true,
|
||||
"terminal.integrated.suggest.enabled": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// File Exclusions (Reduce Noise)
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"files.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/.vs": true,
|
||||
"**/node_modules": true,
|
||||
"**/.git": false
|
||||
},
|
||||
|
||||
"search.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/node_modules": true,
|
||||
"**/.vs": true,
|
||||
"**/coverage": true
|
||||
},
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// .NET Specific Settings
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"omnisharp.enableEditorConfigSupport": true,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"dotnet.backgroundAnalysis.enabled": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Testing Integration
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"dotnet.defaultSolution": "QuanTAlib.sln",
|
||||
"dotnet.testController.enabled": true,
|
||||
"dotnet.testExplorer.enabled": true,
|
||||
"dotnet-test-explorer.autoExpandTree": true,
|
||||
"dotnet-test-explorer.autoWatch": false,
|
||||
"dotnet-test-explorer.runAfterBuild": false,
|
||||
"dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true,
|
||||
"dotnet.server.useOmnisharp": false,
|
||||
|
||||
"testing.automaticallyOpenPeekView": "never",
|
||||
"testing.openTesting": "neverOpen",
|
||||
"testing.automaticallyOpenTestResults": "neverOpen"
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// Keyboard Shortcuts Reference
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// Tab - Accept inline suggestion
|
||||
// Ctrl+? - Accept next word
|
||||
// Ctrl+Enter - Accept line
|
||||
// Esc - Dismiss suggestion
|
||||
// Alt+] - Next suggestion
|
||||
// Alt+[ - Previous suggestion
|
||||
// Ctrl+I - Open Copilot Chat
|
||||
//
|
||||
// Quality Control Reminders:
|
||||
// ? Review all Copilot suggestions for optimization patterns
|
||||
// ? Run tests after accepting: dotnet test
|
||||
// ? Check performance impact with benchmarks
|
||||
// ? Validate against reference implementations
|
||||
}
|
||||
{
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// GitHub Copilot Settings for QuanTAlib Workspace
|
||||
// Optimized for high-performance financial library development
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Copilot Core Settings
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Enable Copilot completions (suggestions appear automatically)
|
||||
"github.copilot.editor.enableAutoCompletions": true,
|
||||
|
||||
// Enable Copilot for all file types
|
||||
"github.copilot.enable": {
|
||||
"*": true,
|
||||
"plaintext": false,
|
||||
"markdown": true,
|
||||
"scminput": false
|
||||
},
|
||||
|
||||
// Show inline suggestions
|
||||
"editor.inlineSuggest.enabled": true,
|
||||
|
||||
// Always show the inline suggestion toolbar
|
||||
"editor.inlineSuggest.showToolbar": "always",
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Copilot Chat Settings (Manual Review Required)
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// DO NOT auto-apply chat edits - require manual review for quality control
|
||||
"chat.editing.autoApply": "off",
|
||||
|
||||
// Confirm before removing edit requests
|
||||
"chat.editing.confirmEditRequestRemoval": true,
|
||||
|
||||
// Show chat panel on the side
|
||||
"chat.editor.wordWrap": "on",
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Editor Settings for Productivity
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Enable quick suggestions in all contexts
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": true,
|
||||
"strings": true
|
||||
},
|
||||
|
||||
// Show suggestions on trigger characters
|
||||
"editor.suggestOnTriggerCharacters": true,
|
||||
|
||||
// Accept suggestion on commit character (like dot, parenthesis)
|
||||
"editor.acceptSuggestionOnCommitCharacter": true,
|
||||
|
||||
// Faster suggestion appearance
|
||||
"editor.quickSuggestionsDelay": 0,
|
||||
|
||||
// Show snippet suggestions with other suggestions
|
||||
"editor.snippetSuggestions": "inline",
|
||||
|
||||
// Tab key behavior
|
||||
"editor.tabCompletion": "on",
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// C# Specific Settings
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"[csharp]": {
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnPaste": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": true,
|
||||
"strings": true
|
||||
}
|
||||
},
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Performance & Quality Control
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Save automatically (helps with Copilot context)
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.autoSaveDelay": 1000,
|
||||
|
||||
// Show whitespace (important for performance-critical code)
|
||||
"editor.renderWhitespace": "boundary",
|
||||
|
||||
// Show inline parameter hints
|
||||
"editor.inlayHints.enabled": "on",
|
||||
|
||||
// Highlight matching brackets
|
||||
"editor.bracketPairColorization.enabled": true,
|
||||
"editor.guides.bracketPairs": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Git Integration
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
// Auto-fetch git changes
|
||||
"git.autofetch": true,
|
||||
|
||||
// Confirm before synchronizing
|
||||
"git.confirmSync": false,
|
||||
|
||||
// Show inline blame
|
||||
"git.decorations.enabled": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Terminal Settings (Preserved from original)
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"terminal.integrated.defaultProfile.windows": "PowerShell",
|
||||
"terminal.integrated.profiles.windows": {
|
||||
"PowerShell": {
|
||||
"source": "PowerShell",
|
||||
"icon": "terminal-powershell"
|
||||
}
|
||||
},
|
||||
"terminal.integrated.shellIntegration.enabled": true,
|
||||
"terminal.integrated.suggest.enabled": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// File Exclusions (Reduce Noise)
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"files.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/.vs": true,
|
||||
"**/node_modules": true,
|
||||
"**/.git": false
|
||||
},
|
||||
|
||||
"search.exclude": {
|
||||
"**/bin": true,
|
||||
"**/obj": true,
|
||||
"**/node_modules": true,
|
||||
"**/.vs": true,
|
||||
"**/coverage": true
|
||||
},
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// .NET Specific Settings
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"omnisharp.enableEditorConfigSupport": true,
|
||||
"omnisharp.enableRoslynAnalyzers": true,
|
||||
"dotnet.backgroundAnalysis.enabled": true,
|
||||
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
// Testing Integration
|
||||
// ?????????????????????????????????????????????????????????????????
|
||||
|
||||
"dotnet.defaultSolution": "QuanTAlib.sln",
|
||||
"dotnet.testController.enabled": true,
|
||||
"dotnet.unitTests.runSettingsPath": "",
|
||||
"dotnet.completion.showCompletionItemsFromUnimportedNamespaces": true,
|
||||
"dotnet.server.useOmnisharp": false,
|
||||
|
||||
"testing.automaticallyOpenPeekView": "never",
|
||||
"testing.openTesting": "neverOpen",
|
||||
"testing.automaticallyOpenTestResults": "neverOpen",
|
||||
"sonarlint.connectedMode.project": {
|
||||
"connectionId": "mihakralj-quantalib",
|
||||
"projectKey": "mihakralj_QuanTAlib"
|
||||
}
|
||||
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// Keyboard Shortcuts Reference
|
||||
// ???????????????????????????????????????????????????????????????????
|
||||
// Tab - Accept inline suggestion
|
||||
// Ctrl+? - Accept next word
|
||||
// Ctrl+Enter - Accept line
|
||||
// Esc - Dismiss suggestion
|
||||
// Alt+] - Next suggestion
|
||||
// Alt+[ - Previous suggestion
|
||||
// Ctrl+I - Open Copilot Chat
|
||||
//
|
||||
// Quality Control Reminders:
|
||||
// ? Review all Copilot suggestions for optimization patterns
|
||||
// ? Run tests after accepting: dotnet test
|
||||
// ? Check performance impact with benchmarks
|
||||
// ? Validate against reference implementations
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -7,7 +7,7 @@
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/QuanTAlib.Tests/QuanTAlib.Tests.csproj",
|
||||
"${workspaceFolder}/lib/QuanTAlib.Tests.csproj",
|
||||
"--framework",
|
||||
"net10.0"
|
||||
],
|
||||
@@ -27,7 +27,7 @@
|
||||
"type": "process",
|
||||
"args": [
|
||||
"test",
|
||||
"${workspaceFolder}/tests/QuanTAlib.Tests/QuanTAlib.Tests.csproj"
|
||||
"${workspaceFolder}/lib/QuanTAlib.Tests.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "test",
|
||||
|
||||
+60
-60
@@ -1,60 +1,60 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<NoWarn>$(NoWarn);NU1903;NU5104;NETSDK1057</NoWarn>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Deterministic>true</Deterministic>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
|
||||
<DebugType>full</DebugType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<IsLocalBuild Condition="'$(GITHUB_ACTIONS)' == ''">true</IsLocalBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>link</TrimMode>
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<Deterministic>true</Deterministic>
|
||||
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
|
||||
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
<UseSystemResourceKeys>true</UseSystemResourceKeys>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
|
||||
<PackageReference Include="Microsoft.DotNet.Interactive.Formatting" Version="1.0.0-beta.25323.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsLocalBuild)' == 'true' AND $([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<QuantowerRoot>Z:\Quantower</QuantowerRoot>
|
||||
<QuantowerPath>$([System.IO.Directory]::GetDirectories("$(QuantowerRoot)\TradingPlatform", "v1*")[0])</QuantowerPath>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<LangVersion>preview</LangVersion>
|
||||
<NoWarn>$(NoWarn);NU1903;NU5104;NETSDK1057</NoWarn>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<Deterministic>true</Deterministic>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<ProduceReferenceAssembly>False</ProduceReferenceAssembly>
|
||||
<DebugType>full</DebugType>
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<IsLocalBuild Condition="'$(GITHUB_ACTIONS)' == ''">true</IsLocalBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>link</TrimMode>
|
||||
<PublishAot>true</PublishAot>
|
||||
<PublishReadyToRun>true</PublishReadyToRun>
|
||||
<TieredCompilation>true</TieredCompilation>
|
||||
<DebugType>portable</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<Deterministic>true</Deterministic>
|
||||
<EnableUnsafeBinaryFormatterSerialization>false</EnableUnsafeBinaryFormatterSerialization>
|
||||
<EnableUnsafeUTF7Encoding>false</EnableUnsafeUTF7Encoding>
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
<UseSystemResourceKeys>true</UseSystemResourceKeys>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>S1944,S2053,S2222,S2259,S2583,S2589,S3329,S3655,S3900,S3949,S3966,S4158,S4347,S5773,S6781</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/>
|
||||
<PackageReference Include="Microsoft.DotNet.Interactive.Formatting" Version="1.0.0-beta.25323.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(IsLocalBuild)' == 'true' AND $([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<QuantowerRoot>Z:\Quantower</QuantowerRoot>
|
||||
<QuantowerPath>$([System.IO.Directory]::GetDirectories("$(QuantowerRoot)\TradingPlatform", "v1*")[0])</QuantowerPath>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+26
-26
@@ -1,26 +1,26 @@
|
||||
assembly-versioning-scheme: MajorMinorPatch
|
||||
assembly-file-versioning-scheme: MajorMinorPatch
|
||||
major-version-bump-message: '\+semver:\s?(breaking|major)'
|
||||
minor-version-bump-message: '\+semver:\s?(feature|minor)'
|
||||
patch-version-bump-message: '\+semver:\s?(fix|patch)'
|
||||
no-bump-message: '\+semver:\s?(none|skip)'
|
||||
tag-prefix: '[vV]'
|
||||
branches:
|
||||
main:
|
||||
regex: ^main$
|
||||
mode: ContinuousDeployment
|
||||
increment: Patch
|
||||
track-merge-target: false
|
||||
is-release-branch: true
|
||||
pre-release-weight: 0
|
||||
develop:
|
||||
regex: ^dev(elop)?(ment)?$
|
||||
mode: ContinuousDelivery
|
||||
increment: Patch
|
||||
track-merge-target: true
|
||||
is-release-branch: false
|
||||
source-branches: ['main']
|
||||
pre-release-weight: 30000
|
||||
ignore:
|
||||
sha: []
|
||||
merge-message-formats: {}
|
||||
assembly-versioning-scheme: MajorMinorPatch
|
||||
assembly-file-versioning-scheme: MajorMinorPatch
|
||||
major-version-bump-message: '\+semver:\s?(breaking|major)'
|
||||
minor-version-bump-message: '\+semver:\s?(feature|minor)'
|
||||
patch-version-bump-message: '\+semver:\s?(fix|patch)'
|
||||
no-bump-message: '\+semver:\s?(none|skip)'
|
||||
tag-prefix: '[vV]'
|
||||
branches:
|
||||
main:
|
||||
regex: ^main$
|
||||
mode: ContinuousDeployment
|
||||
increment: Patch
|
||||
track-merge-target: false
|
||||
is-release-branch: true
|
||||
pre-release-weight: 0
|
||||
develop:
|
||||
regex: ^dev(elop)?(ment)?$
|
||||
mode: ContinuousDelivery
|
||||
increment: Patch
|
||||
track-merge-target: true
|
||||
is-release-branch: false
|
||||
source-branches: ['main']
|
||||
pre-release-weight: 30000
|
||||
ignore:
|
||||
sha: []
|
||||
merge-message-formats: {}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
[](https://sonarcloud.io/summary/overall?id=mihakralj_QuanTAlib)
|
||||
[](https://app.codacy.com/gh/mihakralj/QuanTAlib/dashboard)
|
||||
[](https://codecov.io/gh/mihakralj/QuanTAlib)
|
||||
[](https://sonarcloud.io/summary/new_code?id=mihakralj_QuanTAlib)
|
||||
[](https://www.codefactor.io/repository/github/mihakralj/quantalib/overview/main)
|
||||
|
||||
[](https://www.nuget.org/packages/QuanTAlib/)
|
||||

|
||||
[](https://www.nuget.org/packages/QuanTAlib/)
|
||||
[](https://github.com/mihakralj/QuanTAlib/watchers)
|
||||
[](https://dotnet.microsoft.com/en-us/download/dotnet)
|
||||
|
||||
# QuanTAlib - Quantitative Technical Analysis Library
|
||||
|
||||
**Quan**titative **TA** **lib**rary (QuanTAlib) is a high-performance C# library for quantitative technical analysis, designed for [Quantower](https://www.quantower.com/) and other C#-based trading platforms.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Real-time streaming** - Indicators calculate results from incoming data without re-processing history
|
||||
- **Update/correction support** - Last value can be recalculated multiple times before advancing to next bar
|
||||
- **Valid from first bar** - Mathematically correct results from the first value with `IsHot` warmup indicator
|
||||
- **SIMD-optimized** - Hardware-accelerated vector operations (AVX/SSE) for batch processing
|
||||
- **Zero-allocation hot paths** - Minimal GC pressure for high-frequency scenarios
|
||||
|
||||
## Architecture
|
||||
|
||||
QuanTAlib uses a **Structure of Arrays (SoA)** memory layout optimized for numerical computing:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Core Data Types │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ TValue (16 bytes) │ Time-value pair (long + double) │
|
||||
│ TBar (48 bytes) │ OHLCV bar (long + 5 doubles) │
|
||||
│ TSeries │ Time series with SoA layout │
|
||||
│ TBarSeries │ OHLCV series with SoA layout │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Data Feeds │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ IFeed │ Unified feed interface │
|
||||
│ GBM │ Geometric Brownian Motion sim │
|
||||
│ CsvFeed │ CSV file reader │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
### Performance Design
|
||||
|
||||
The SoA layout stores timestamps and values in separate contiguous arrays:
|
||||
|
||||
```csharp
|
||||
// TSeries internal structure
|
||||
protected readonly List<long> _t; // Timestamps (contiguous)
|
||||
protected readonly List<double> _v; // Values (contiguous)
|
||||
|
||||
// Direct SIMD access via Span<T>
|
||||
ReadOnlySpan<double> values = series.Values;
|
||||
double avg = values.AverageSIMD(); // Hardware-accelerated
|
||||
```
|
||||
|
||||
This enables:
|
||||
- **Cache locality** - Sequential memory access patterns
|
||||
- **SIMD vectorization** - Process 4-8 values per CPU instruction
|
||||
- **Zero-copy access** - `CollectionsMarshal.AsSpan()` exposes internal arrays
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
dotnet add package QuanTAlib
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Create EMA indicator
|
||||
var ema = new Ema(period: 10);
|
||||
|
||||
// Streaming mode - process one value at a time
|
||||
TValue result = ema.Update(new TValue(DateTime.Now, price), isNew: true);
|
||||
|
||||
// Update current bar (e.g., price tick within same minute)
|
||||
result = ema.Update(new TValue(DateTime.Now, newPrice), isNew: false);
|
||||
|
||||
// Batch mode - process entire series
|
||||
var series = new TSeries();
|
||||
series.Add(prices); // Add historical data
|
||||
TSeries emaResults = Ema.Calculate(series, period: 10);
|
||||
```
|
||||
|
||||
### Multi-Period Analysis with SIMD
|
||||
|
||||
```csharp
|
||||
// Calculate multiple EMAs in parallel using SIMD
|
||||
int[] periods = { 9, 12, 26 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// Single update calculates all periods
|
||||
TValue[] results = emaVector.Update(new TValue(time, price));
|
||||
Console.WriteLine($"EMA(9)={results[0]}, EMA(12)={results[1]}, EMA(26)={results[2]}");
|
||||
```
|
||||
|
||||
### Using Data Feeds
|
||||
|
||||
```csharp
|
||||
// Geometric Brownian Motion simulator
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2);
|
||||
TBarSeries bars = gbm.Fetch(count: 1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// CSV file reader
|
||||
var csv = new CsvFeed("data/daily_IBM.csv");
|
||||
TBar bar = csv.Next(isNew: true);
|
||||
```
|
||||
|
||||
## Installation to Quantower
|
||||
|
||||
Copy DLL files to Quantower installation:
|
||||
|
||||
```
|
||||
<Quantower_root>\Settings\Scripts\Indicators\QuanTAlib\Averages\Averages.dll
|
||||
```
|
||||
|
||||
Where `<Quantower_root>` is the directory containing `Start.lnk`.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
QuanTAlib/
|
||||
├── lib/
|
||||
│ ├── core/
|
||||
│ │ ├── tvalue/ # TValue struct
|
||||
│ │ ├── tseries/ # TSeries class
|
||||
│ │ ├── tbar/ # TBar struct
|
||||
│ │ ├── tbarseries/ # TBarSeries class
|
||||
│ │ └── simd/ # SIMD extensions
|
||||
│ ├── averages/
|
||||
│ │ └── ema/ # EMA indicator + tests + docs
|
||||
│ └── feeds/
|
||||
│ ├── csv/ # CSV file feed
|
||||
│ └── gbm/ # GBM simulator
|
||||
└── quantower/ # Quantower integration
|
||||
```
|
||||
|
||||
Each indicator follows a consistent file pattern:
|
||||
- `Indicator.cs` - Core implementation
|
||||
- `Indicator.Tests.cs` - Unit tests
|
||||
- `Indicator.Validation.Tests.cs` - Cross-validation with other libraries
|
||||
- `Indicator.md` - Documentation
|
||||
- `Indicator.Notebook.dib` - Interactive notebook
|
||||
- `Indicator.Quantower.cs` - Quantower wrapper
|
||||
|
||||
## Validation
|
||||
|
||||
QuanTAlib validates results against established TA libraries:
|
||||
|
||||
- [TA-LIB](https://www.ta-lib.org/function.html) - Industry standard C library
|
||||
- [Skender Stock Indicators](https://dotnet.stockindicators.dev/) - Popular .NET library
|
||||
- [Tulip Indicators](https://tulipindicators.org/) - High-performance C library
|
||||
|
||||
## Requirements
|
||||
|
||||
- .NET 8.0, 9.0, or 10.0
|
||||
- Hardware with AVX/SSE support recommended for optimal SIMD performance
|
||||
|
||||
## License
|
||||
|
||||
Apache License 2.0 - See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Each indicator should include:
|
||||
1. Core implementation with streaming support
|
||||
2. Unit tests covering edge cases
|
||||
3. Validation tests against reference libraries
|
||||
4. Documentation with mathematical formulas
|
||||
5. Quantower wrapper (optional)
|
||||
|
||||
## Links
|
||||
|
||||
- [GitHub Repository](https://github.com/mihakralj/QuanTAlib)
|
||||
- [NuGet Package](https://www.nuget.org/packages/QuanTAlib/)
|
||||
- [Quantower Platform](https://www.quantower.com/)
|
||||
+114089
-114089
File diff suppressed because one or more lines are too long
Binary file not shown.
+13
-13
@@ -1,13 +1,13 @@
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'QuanTAlib.Tests'">
|
||||
<BaseIntermediateOutputPath>obj\tests\</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>bin\tests\</BaseOutputPath>
|
||||
<OutputPath>bin\tests\$(Configuration)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
<Project>
|
||||
<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AppendTargetFrameworkToOutputPath>true</AppendTargetFrameworkToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildProjectName)' == 'QuanTAlib.Tests'">
|
||||
<BaseIntermediateOutputPath>obj\tests\</BaseIntermediateOutputPath>
|
||||
<BaseOutputPath>bin\tests\</BaseOutputPath>
|
||||
<OutputPath>bin\tests\$(Configuration)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
+42
-46
@@ -1,46 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<TargetFrameworks></TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<EnableMicrosoftTestingPlatform>false</EnableMicrosoftTestingPlatform>
|
||||
<TestingPlatformDotnetTestSupport>false</TestingPlatformDotnetTestSupport>
|
||||
<NoWarn>$(NoWarn);CS8892</NoWarn>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Skender.Stock.Indicators" Version="2.7.0" />
|
||||
<PackageReference Include="TALib.NETCore" Version="0.5.0" />
|
||||
<PackageReference Include="Tulip.NETCore" Version="0.8.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="1.6.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="quantalib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.Tests.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="feeds\csv\daily_IBM.csv">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>daily_IBM.csv</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<NoWarn>$(NoWarn);CS8892</NoWarn>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Skender.Stock.Indicators" Version="2.7.0" />
|
||||
<PackageReference Include="TALib.NETCore" Version="0.5.0" />
|
||||
<PackageReference Include="Tulip.NETCore" Version="0.8.0.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="quantalib.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.Tests.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="feeds\csv\daily_IBM.csv">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<Link>daily_IBM.csv</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
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;
|
||||
|
||||
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();
|
||||
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
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
this.PaintSmoothCurve(args, Series!, 0, 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;
|
||||
|
||||
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();
|
||||
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
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
this.PaintSmoothCurve(args, Series!, 0, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
+228
-228
@@ -1,228 +1,228 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Ema_Constructor_Period_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ema(0));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(-1));
|
||||
|
||||
var ema = new Ema(10);
|
||||
Assert.NotNull(ema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Constructor_Alpha_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ema(0.0));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(-0.1));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(1.1));
|
||||
|
||||
var ema = new Ema(0.5);
|
||||
Assert.NotNull(ema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_ReturnsValue()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
|
||||
TValue result = ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100), isNew: true);
|
||||
double value1 = ema.Value;
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 105), isNew: true);
|
||||
double value2 = ema.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
ema.Update(new TValue(DateTime.Now, 110), isNew: true);
|
||||
double beforeUpdate = ema.Value;
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 120), isNew: false);
|
||||
double afterUpdate = ema.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Reset_ClearsState()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
ema.Update(new TValue(DateTime.Now, 105));
|
||||
double valueBefore = ema.Value;
|
||||
|
||||
ema.Reset();
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
ema.Update(new TValue(DateTime.Now, 50));
|
||||
Assert.NotEqual(0, ema.Value.Value);
|
||||
Assert.NotEqual(valueBefore, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Properties_Accessible()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
Assert.False(ema.IsHot);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.NotEqual(0, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
// Initially IsHot should be false
|
||||
Assert.False(ema.IsHot);
|
||||
|
||||
// Feed values until it warms up
|
||||
// Warmup condition is state.E <= 1e-10
|
||||
// state.E starts at 1.0 and decays by (1 - alpha) each step
|
||||
// alpha = 2 / (10 + 1) = 2/11 ~= 0.1818
|
||||
// (1 - alpha) ~= 0.8181
|
||||
// 1.0 * (0.8181)^n <= 1e-10
|
||||
// n * log(0.8181) <= log(1e-10)
|
||||
// n * -0.200 <= -23.02
|
||||
// n >= 115 steps roughly
|
||||
|
||||
int steps = 0;
|
||||
while (!ema.IsHot && steps < 1000)
|
||||
{
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(ema.IsHot);
|
||||
Assert.True(steps > 0); // Should take some steps
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_PeriodEquivalence_BothConstructorsWork()
|
||||
{
|
||||
int period = 20;
|
||||
double alpha = 2.0 / (period + 1);
|
||||
|
||||
var emaPeriod = new Ema(period);
|
||||
var emaAlpha = new Ema(alpha);
|
||||
|
||||
// Both should accept Calc calls and produce same result
|
||||
TValue result1 = emaPeriod.Update(new TValue(DateTime.Now, 100));
|
||||
TValue result2 = emaAlpha.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
ema.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember EMA state after 10 values
|
||||
double emaAfterTen = ema.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
ema.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalEma = ema.Update(tenthInput, isNew: false);
|
||||
|
||||
// EMA should match the original state after 10 values
|
||||
Assert.Equal(emaAfterTen, finalEma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var emaIterative = new Ema(10);
|
||||
var emaBatch = new Ema(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Generate data
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(emaIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = emaBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Result_ImplicitConversionToDouble()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
// This should compile and work because TValue has implicit conversion to double
|
||||
double result = ema.Value;
|
||||
|
||||
Assert.Equal(100.0, result, 1e-10);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Ema_Constructor_Period_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ema(0));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(-1));
|
||||
|
||||
var ema = new Ema(10);
|
||||
Assert.NotNull(ema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Constructor_Alpha_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Ema(0.0));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(-0.1));
|
||||
Assert.Throws<ArgumentException>(() => new Ema(1.1));
|
||||
|
||||
var ema = new Ema(0.5);
|
||||
Assert.NotNull(ema);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_ReturnsValue()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
|
||||
TValue result = ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100), isNew: true);
|
||||
double value1 = ema.Value;
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 105), isNew: true);
|
||||
double value2 = ema.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
ema.Update(new TValue(DateTime.Now, 110), isNew: true);
|
||||
double beforeUpdate = ema.Value;
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 120), isNew: false);
|
||||
double afterUpdate = ema.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Reset_ClearsState()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
ema.Update(new TValue(DateTime.Now, 105));
|
||||
double valueBefore = ema.Value;
|
||||
|
||||
ema.Reset();
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
ema.Update(new TValue(DateTime.Now, 50));
|
||||
Assert.NotEqual(0, ema.Value.Value);
|
||||
Assert.NotEqual(valueBefore, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Properties_Accessible()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
Assert.Equal(0, ema.Value.Value);
|
||||
Assert.False(ema.IsHot);
|
||||
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.NotEqual(0, ema.Value.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
|
||||
// Initially IsHot should be false
|
||||
Assert.False(ema.IsHot);
|
||||
|
||||
// Feed values until it warms up
|
||||
// Warmup condition is state.E <= 1e-10
|
||||
// state.E starts at 1.0 and decays by (1 - alpha) each step
|
||||
// alpha = 2 / (10 + 1) = 2/11 ~= 0.1818
|
||||
// (1 - alpha) ~= 0.8181
|
||||
// 1.0 * (0.8181)^n <= 1e-10
|
||||
// n * log(0.8181) <= log(1e-10)
|
||||
// n * -0.200 <= -23.02
|
||||
// n >= 115 steps roughly
|
||||
|
||||
int steps = 0;
|
||||
while (!ema.IsHot && steps < 1000)
|
||||
{
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
steps++;
|
||||
}
|
||||
|
||||
Assert.True(ema.IsHot);
|
||||
Assert.True(steps > 0); // Should take some steps
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_PeriodEquivalence_BothConstructorsWork()
|
||||
{
|
||||
int period = 20;
|
||||
double alpha = 2.0 / (period + 1);
|
||||
|
||||
var emaPeriod = new Ema(period);
|
||||
var emaAlpha = new Ema(alpha);
|
||||
|
||||
// Both should accept Calc calls and produce same result
|
||||
TValue result1 = emaPeriod.Update(new TValue(DateTime.Now, 100));
|
||||
TValue result2 = emaAlpha.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
Assert.Equal(result1.Value, result2.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
ema.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember EMA state after 10 values
|
||||
double emaAfterTen = ema.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
ema.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalEma = ema.Update(tenthInput, isNew: false);
|
||||
|
||||
// EMA should match the original state after 10 values
|
||||
Assert.Equal(emaAfterTen, finalEma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var emaIterative = new Ema(10);
|
||||
var emaBatch = new Ema(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Generate data
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(emaIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = emaBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ema_Result_ImplicitConversionToDouble()
|
||||
{
|
||||
var ema = new Ema(10);
|
||||
ema.Update(new TValue(DateTime.Now, 100));
|
||||
|
||||
// This should compile and work because TValue has implicit conversion to double
|
||||
double result = ema.Value;
|
||||
|
||||
Assert.Equal(100.0, result, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,198 +1,198 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaValidationTests : IDisposable
|
||||
{
|
||||
private readonly TBarSeries _bars;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
private readonly Random _rnd = new(42);
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public EmaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate 1000 records using GBM feed
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
_bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// 2. Extract Close TSeries
|
||||
_data = _bars.Close;
|
||||
|
||||
// 3. Prepare data for Skender (List<Quote>)
|
||||
_skenderQuotes = new List<Quote>();
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
_skenderQuotes.Add(new Quote
|
||||
{
|
||||
Date = new DateTime(_bars.Open.Times[i]),
|
||||
Open = (decimal)_bars.Open[i].Value,
|
||||
High = (decimal)_bars.High[i].Value,
|
||||
Low = (decimal)_bars.Low[i].Value,
|
||||
Close = (decimal)_bars.Close[i].Value,
|
||||
Volume = (decimal)_bars.Volume[i].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate Skender EMA
|
||||
var sResult = _skenderQuotes.GetEma(period).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData(qResult, sResult, period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] tData = _data.Select(x => x.Value).ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate TA-Lib EMA
|
||||
var retCode = TALib.Functions.Ema(tData, 0..^0, output, out var outRange, period);
|
||||
|
||||
// Check success
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
// TA-Lib skips the lookback period, so output[0] corresponds to input[lookback]
|
||||
int lookback = TALib.Functions.EmaLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Talib(qResult, output, outRange, lookback, period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] tData = _data.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate Tulip EMA
|
||||
var emaIndicator = Tulip.Indicators.ema;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { (double)period };
|
||||
double[][] outputs = { new double[tData.Length] };
|
||||
|
||||
emaIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData(qResult, tResult.ToList(), period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against Tulip");
|
||||
}
|
||||
|
||||
private void VerifyData(TSeries qSeries, List<double> tSeries, int period)
|
||||
{
|
||||
// Ensure we have enough data
|
||||
Assert.Equal(qSeries.Count, tSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double tValue = tSeries[i];
|
||||
if (tValue == 0) continue;
|
||||
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyData(TSeries qSeries, List<EmaResult> sSeries, int period)
|
||||
{
|
||||
// Ensure we have enough data
|
||||
Assert.Equal(qSeries.Count, sSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double? sValue = sSeries[i].Ema;
|
||||
|
||||
// Skip if Skender returns null (warmup period)
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
// Assert equality with tolerance
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback, int period)
|
||||
{
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
// outRange.End.Value is the number of elements written to tOutput
|
||||
int validCount = outRange.End.Value - outRange.Start.Value;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
|
||||
// Calculate index in tOutput
|
||||
// If i < lookback, we don't have a value from TA-Lib
|
||||
if (i < lookback) continue;
|
||||
|
||||
int tIndex = i - lookback;
|
||||
|
||||
// Check if tIndex is within valid range
|
||||
if (tIndex >= validCount) continue;
|
||||
|
||||
double tValue = tOutput[tIndex];
|
||||
|
||||
// Assert equality with tolerance
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Skender.Stock.Indicators;
|
||||
using TALib;
|
||||
using Tulip;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaValidationTests : IDisposable
|
||||
{
|
||||
private readonly TBarSeries _bars;
|
||||
private readonly TSeries _data;
|
||||
private readonly List<Quote> _skenderQuotes;
|
||||
private readonly Random _rnd = new(42);
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public EmaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
|
||||
// 1. Generate 1000 records using GBM feed
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
_bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
// 2. Extract Close TSeries
|
||||
_data = _bars.Close;
|
||||
|
||||
// 3. Prepare data for Skender (List<Quote>)
|
||||
_skenderQuotes = new List<Quote>();
|
||||
for (int i = 0; i < _bars.Count; i++)
|
||||
{
|
||||
_skenderQuotes.Add(new Quote
|
||||
{
|
||||
Date = new DateTime(_bars.Open.Times[i]),
|
||||
Open = (decimal)_bars.Open[i].Value,
|
||||
High = (decimal)_bars.High[i].Value,
|
||||
Low = (decimal)_bars.Low[i].Value,
|
||||
Close = (decimal)_bars.Close[i].Value,
|
||||
Volume = (decimal)_bars.Volume[i].Value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Skender()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate Skender EMA
|
||||
var sResult = _skenderQuotes.GetEma(period).ToList();
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData(qResult, sResult, period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against Skender");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Talib()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for TA-Lib (double[])
|
||||
double[] tData = _data.Select(x => x.Value).ToArray();
|
||||
double[] output = new double[tData.Length];
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate TA-Lib EMA
|
||||
var retCode = TALib.Functions.Ema(tData, 0..^0, output, out var outRange, period);
|
||||
|
||||
// Check success
|
||||
Assert.Equal(Core.RetCode.Success, retCode);
|
||||
|
||||
// TA-Lib skips the lookback period, so output[0] corresponds to input[lookback]
|
||||
int lookback = TALib.Functions.EmaLookback(period);
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData_Talib(qResult, output, outRange, lookback, period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against TA-Lib");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Tulip()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for Tulip (double[])
|
||||
double[] tData = _data.Select(x => x.Value).ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib EMA
|
||||
var ema = new global::QuanTAlib.Ema(period);
|
||||
var qResult = ema.Update(_data);
|
||||
|
||||
// Calculate Tulip EMA
|
||||
var emaIndicator = Tulip.Indicators.ema;
|
||||
double[][] inputs = { tData };
|
||||
double[] options = { (double)period };
|
||||
double[][] outputs = { new double[tData.Length] };
|
||||
|
||||
emaIndicator.Run(inputs, options, outputs);
|
||||
var tResult = outputs[0];
|
||||
|
||||
// Compare last 100 records
|
||||
VerifyData(qResult, tResult.ToList(), period);
|
||||
}
|
||||
_output.WriteLine("EMA validated successfully against Tulip");
|
||||
}
|
||||
|
||||
private void VerifyData(TSeries qSeries, List<double> tSeries, int period)
|
||||
{
|
||||
// Ensure we have enough data
|
||||
Assert.Equal(qSeries.Count, tSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double tValue = tSeries[i];
|
||||
if (tValue == 0) continue;
|
||||
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyData(TSeries qSeries, List<EmaResult> sSeries, int period)
|
||||
{
|
||||
// Ensure we have enough data
|
||||
Assert.Equal(qSeries.Count, sSeries.Count);
|
||||
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
double? sValue = sSeries[i].Ema;
|
||||
|
||||
// Skip if Skender returns null (warmup period)
|
||||
if (!sValue.HasValue) continue;
|
||||
|
||||
// Assert equality with tolerance
|
||||
Assert.Equal(sValue.Value, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
private void VerifyData_Talib(TSeries qSeries, double[] tOutput, Range outRange, int lookback, int period)
|
||||
{
|
||||
int count = qSeries.Count;
|
||||
int skip = count - 100; // Last 100 records
|
||||
|
||||
// outRange.End.Value is the number of elements written to tOutput
|
||||
int validCount = outRange.End.Value - outRange.Start.Value;
|
||||
|
||||
for (int i = skip; i < count; i++)
|
||||
{
|
||||
double qValue = qSeries[i].Value;
|
||||
|
||||
// Calculate index in tOutput
|
||||
// If i < lookback, we don't have a value from TA-Lib
|
||||
if (i < lookback) continue;
|
||||
|
||||
int tIndex = i - lookback;
|
||||
|
||||
// Check if tIndex is within valid range
|
||||
if (tIndex >= validCount) continue;
|
||||
|
||||
double tValue = tOutput[tIndex];
|
||||
|
||||
// Assert equality with tolerance
|
||||
Assert.Equal(tValue, qValue, 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+163
-163
@@ -1,163 +1,163 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public struct EmaState
|
||||
{
|
||||
public double Ema;
|
||||
public double E;
|
||||
public bool IsHot;
|
||||
|
||||
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exponential Moving Average (EMA) - IIR filter with exponential warmup compensator.
|
||||
/// Provides valid output from first bar with O(1) complexity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Algorithm uses exponential smoothing with compensator for immediate valid results.
|
||||
/// Reference: https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.md
|
||||
/// </remarks>
|
||||
public class Ema
|
||||
{
|
||||
private readonly double _alpha;
|
||||
private EmaState _state = EmaState.New();
|
||||
private EmaState _p_state = EmaState.New();
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified period.
|
||||
/// Alpha = 2 / (period + 1)
|
||||
/// </summary>
|
||||
/// <param name="period">Period for EMA calculation (must be > 0)</param>
|
||||
public Ema(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_alpha = 2.0 / (period + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified alpha smoothing factor.
|
||||
/// </summary>
|
||||
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
|
||||
public Ema(double alpha)
|
||||
{
|
||||
if (alpha <= 0 || alpha > 1)
|
||||
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
|
||||
|
||||
_alpha = alpha;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA value.
|
||||
/// </summary>
|
||||
public TValue Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the EMA has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _state.IsHot;
|
||||
|
||||
/// <summary>
|
||||
/// Core EMA calculation kernel.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Compute(double input, double alpha, ref EmaState state)
|
||||
{
|
||||
state.Ema += alpha * (input - state.Ema);
|
||||
|
||||
if (!state.IsHot)
|
||||
{
|
||||
state.E *= (1.0 - alpha);
|
||||
state.IsHot = state.E <= 1e-10;
|
||||
return state.Ema / (1.0 - state.E);
|
||||
}
|
||||
|
||||
return state.Ema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
|
||||
/// <returns>Compensated EMA value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double val = Compute(input.Value, _alpha, ref _state);
|
||||
Value = new TValue(input.Time, val);
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(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;
|
||||
|
||||
// Local state for batch processing
|
||||
EmaState state = _state;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = Compute(sourceValues[i], _alpha, ref state);
|
||||
tSpan[i] = sourceTimes[i];
|
||||
vSpan[i] = val;
|
||||
}
|
||||
|
||||
// Update instance state to the final state
|
||||
_state = state;
|
||||
_p_state = state; // Assume last point is committed
|
||||
|
||||
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMA for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="period">EMA period</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var ema = new Ema(period);
|
||||
return ema.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the EMA state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_state = EmaState.New();
|
||||
_p_state = _state;
|
||||
Value = default;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public struct EmaState
|
||||
{
|
||||
public double Ema;
|
||||
public double E;
|
||||
public bool IsHot;
|
||||
|
||||
public static EmaState New() => new() { Ema = 0, E = 1.0, IsHot = false };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exponential Moving Average (EMA) - IIR filter with exponential warmup compensator.
|
||||
/// Provides valid output from first bar with O(1) complexity.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Algorithm uses exponential smoothing with compensator for immediate valid results.
|
||||
/// Reference: https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.md
|
||||
/// </remarks>
|
||||
public class Ema
|
||||
{
|
||||
private readonly double _alpha;
|
||||
private EmaState _state = EmaState.New();
|
||||
private EmaState _p_state = EmaState.New();
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified period.
|
||||
/// Alpha = 2 / (period + 1)
|
||||
/// </summary>
|
||||
/// <param name="period">Period for EMA calculation (must be > 0)</param>
|
||||
public Ema(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_alpha = 2.0 / (period + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates EMA with specified alpha smoothing factor.
|
||||
/// </summary>
|
||||
/// <param name="alpha">Smoothing factor (0 < alpha <= 1)</param>
|
||||
public Ema(double alpha)
|
||||
{
|
||||
if (alpha <= 0 || alpha > 1)
|
||||
throw new ArgumentException("Alpha must be between 0 and 1", nameof(alpha));
|
||||
|
||||
_alpha = alpha;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA value.
|
||||
/// </summary>
|
||||
public TValue Value { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the EMA has warmed up and is providing valid results.
|
||||
/// </summary>
|
||||
public bool IsHot => _state.IsHot;
|
||||
|
||||
/// <summary>
|
||||
/// Core EMA calculation kernel.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Compute(double input, double alpha, ref EmaState state)
|
||||
{
|
||||
state.Ema += alpha * (input - state.Ema);
|
||||
|
||||
if (!state.IsHot)
|
||||
{
|
||||
state.E *= (1.0 - alpha);
|
||||
state.IsHot = state.E <= 1e-10;
|
||||
return state.Ema / (1.0 - state.E);
|
||||
}
|
||||
|
||||
return state.Ema;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <param name="isNew">True for new bar, false for update to current bar (default: true)</param>
|
||||
/// <returns>Compensated EMA value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
}
|
||||
|
||||
double val = Compute(input.Value, _alpha, ref _state);
|
||||
Value = new TValue(input.Time, val);
|
||||
return Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMA with the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(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;
|
||||
|
||||
// Local state for batch processing
|
||||
EmaState state = _state;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double val = Compute(sourceValues[i], _alpha, ref state);
|
||||
tSpan[i] = sourceTimes[i];
|
||||
vSpan[i] = val;
|
||||
}
|
||||
|
||||
// Update instance state to the final state
|
||||
_state = state;
|
||||
_p_state = state; // Assume last point is committed
|
||||
|
||||
Value = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMA for the entire series using a new instance.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="period">EMA period</param>
|
||||
/// <returns>EMA series</returns>
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var ema = new Ema(period);
|
||||
return ema.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the EMA state.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_state = EmaState.New();
|
||||
_p_state = _state;
|
||||
Value = default;
|
||||
}
|
||||
}
|
||||
|
||||
+136
-136
@@ -1,136 +1,136 @@
|
||||
# EMA: Exponential Moving Average
|
||||
|
||||
[Pine Script Implementation of EMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.pine)
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Exponential Moving Average (EMA) is a fundamental technical indicator that calculates the average price over a specific period while giving more weight to recent price data. Introduced in the 1950s, EMA has become one of the most widely used technical indicators in financial markets due to its balance of responsiveness and stability.
|
||||
|
||||
Unlike the Simple Moving Average (SMA) which assigns equal weight to all data points, the EMA emphasizes recent price action, allowing traders to identify trend changes earlier while still filtering out short-term market noise. Its mathematical elegance has made it a standard tool in signal processing beyond finance, including communications, control systems, and data analysis.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Weighted price action:** EMA gives greater importance to recent prices through exponential weighting, providing a more timely response to current market conditions
|
||||
* **Smoothing mechanism:** Acts as a noise filter by reducing the impact of random price fluctuations while preserving meaningful trends
|
||||
* **Universal application:** Functions effectively across all timeframes from intraday to monthly charts, with parameter adjustments
|
||||
* **Foundation indicator:** Serves as the mathematical basis for numerous other technical indicators (MACD, PPO, etc.)
|
||||
|
||||
EMA achieves its enhanced responsiveness by applying a smoothing factor (α) that determines how quickly older data points lose influence. This approach creates a moving average that reacts faster to price changes than an SMA of the same length while maintaining enough stability to identify the underlying trend.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
|-----------|---------|----------|---------------|
|
||||
| Length | 20 | Controls responsiveness/smoothness | Shorter for faster signals in active markets, longer for stable trends in ranging markets |
|
||||
| Source | Close | Data point used for calculation | Change to HL2 or HLC3 for more balanced price representation |
|
||||
| Alpha | 2/(length+1) | Determines weighting decay | Direct alpha manipulation allows for precise tuning beyond standard length settings |
|
||||
|
||||
**Pro Tip:** Many professional traders use multiple EMAs simultaneously (e.g., 8, 21, 50) to identify potential support/resistance levels and trend strength based on their relative positioning.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
EMA works by calculating a weighted average where recent prices have more influence. The implementation uses an optimized form of the EMA calculation that is both computationally efficient and numerically stable.
|
||||
|
||||
**Technical formula:**
|
||||
The optimized EMA formula used in the implementation is:
|
||||
$$EMA_t = \alpha \cdot P_t + (1 - \alpha) \cdot EMA_{t-1}$$
|
||||
|
||||
Where:
|
||||
|
||||
* $\alpha = \frac{2}{N + 1}$ is the smoothing factor ($N$ is the period)
|
||||
* $P_t$ is the current price value
|
||||
* $EMA_{t-1}$ is the previous period's EMA value
|
||||
|
||||
This form is algebraically equivalent to the traditional EMA formula but offers better computational efficiency and numerical stability.
|
||||
|
||||
> 🔍 **Technical Note:** The implementation uses a sophisticated warm-up compensation method that provides accurate EMA values from the first bar. The compensation works by tracking an error term that decays exponentially:
|
||||
> $$e_t = e_{t-1} \cdot (1 - \alpha)$$
|
||||
> $$Compensation = \frac{1}{1 - e_t}$$
|
||||
> $$EMA_{corrected} = Compensation \cdot EMA_{raw}$$
|
||||
> This compensation automatically adjusts during the warm-up phase and becomes negligible ($e \le 1e^{-10}$) once sufficient data has been processed, ensuring mathematically correct values throughout the entire data series without requiring a traditional warm-up period.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides two implementations: a standard scalar version and a SIMD-optimized vector version for high-performance scenarios.
|
||||
|
||||
### Single EMA (`Ema`)
|
||||
|
||||
The `Ema` class calculates a single exponential moving average.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with period 10
|
||||
var ema = new Ema(10);
|
||||
|
||||
// Or initialize with specific alpha
|
||||
var emaAlpha = new Ema(0.5);
|
||||
|
||||
// Streaming update
|
||||
TValue result = ema.Update(new TValue(time, price));
|
||||
Console.WriteLine($"Current EMA: {result.Value}");
|
||||
|
||||
// Access current value property
|
||||
Console.WriteLine($"Current Value: {ema.Value.Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries results = Ema.Calculate(source, 10);
|
||||
```
|
||||
|
||||
### Multi-Alpha EMA (`EmaVector`)
|
||||
|
||||
The `EmaVector` class is a SIMD-optimized implementation for calculating multiple EMAs with different periods on the same input series simultaneously. It leverages hardware intrinsics (AVX/SSE) for high performance.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with multiple periods
|
||||
int[] periods = { 9, 12, 26 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// Streaming update
|
||||
TValue[] results = emaVector.Update(new TValue(time, price));
|
||||
|
||||
// Access values
|
||||
Console.WriteLine($"EMA(9): {results[0].Value}");
|
||||
Console.WriteLine($"EMA(12): {results[1].Value}");
|
||||
Console.WriteLine($"EMA(26): {results[2].Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries[] seriesResults = emaVector.Calculate(source);
|
||||
```
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
|
||||
* **SIMD Optimization:** `EmaVector` processes multiple periods in parallel using vector instructions, significantly reducing CPU cycles for multi-timeframe analysis.
|
||||
* **Zero Allocation:** The streaming `Update` method is designed to be allocation-free (excluding the return struct).
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
The EMA's primary value comes from its ability to identify trend direction and potential reversal points:
|
||||
|
||||
* When price is above EMA, the short-term trend is generally bullish
|
||||
* When price is below EMA, the short-term trend is generally bearish
|
||||
* When a shorter-period EMA crosses above a longer-period EMA, it often signals the beginning of an uptrend
|
||||
* When a shorter-period EMA crosses below a longer-period EMA, it often signals the beginning of a downtrend
|
||||
* The slope of the EMA indicates trend strength and momentum
|
||||
|
||||
EMAs work particularly well in trending markets but may generate false signals during sideways or choppy conditions. For optimal results, traders typically use EMA crossovers or EMA-price crossovers as part of a broader system that includes volume and momentum confirmation.
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
* **Market conditions:** Less effective in choppy, sideways markets where price constantly crosses the average
|
||||
* **Lag factor:** While less significant than SMA, EMA still exhibits some lag, especially with longer lookback periods
|
||||
* **False signals:** Can produce whipsaws during consolidation phases or range-bound conditions
|
||||
* **Parameter sensitivity:** Small changes in length or alpha can significantly alter behavior
|
||||
* **Complementary tools:** Should be used with momentum indicators (RSI, MACD) or volume indicators for confirmation
|
||||
|
||||
## References
|
||||
|
||||
1. Murphy, J.J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
|
||||
2. Kaufman, P. (2013). *Trading Systems and Methods*, 5th Edition. Wiley Trading.
|
||||
3. Ehlers, J. (2001). *Rocket Science for Traders*. John Wiley & Sons.
|
||||
# EMA: Exponential Moving Average
|
||||
|
||||
[Pine Script Implementation of EMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_IIR/ema.pine)
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Exponential Moving Average (EMA) is a fundamental technical indicator that calculates the average price over a specific period while giving more weight to recent price data. Introduced in the 1950s, EMA has become one of the most widely used technical indicators in financial markets due to its balance of responsiveness and stability.
|
||||
|
||||
Unlike the Simple Moving Average (SMA) which assigns equal weight to all data points, the EMA emphasizes recent price action, allowing traders to identify trend changes earlier while still filtering out short-term market noise. Its mathematical elegance has made it a standard tool in signal processing beyond finance, including communications, control systems, and data analysis.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Weighted price action:** EMA gives greater importance to recent prices through exponential weighting, providing a more timely response to current market conditions
|
||||
* **Smoothing mechanism:** Acts as a noise filter by reducing the impact of random price fluctuations while preserving meaningful trends
|
||||
* **Universal application:** Functions effectively across all timeframes from intraday to monthly charts, with parameter adjustments
|
||||
* **Foundation indicator:** Serves as the mathematical basis for numerous other technical indicators (MACD, PPO, etc.)
|
||||
|
||||
EMA achieves its enhanced responsiveness by applying a smoothing factor (α) that determines how quickly older data points lose influence. This approach creates a moving average that reacts faster to price changes than an SMA of the same length while maintaining enough stability to identify the underlying trend.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
|-----------|---------|----------|---------------|
|
||||
| Length | 20 | Controls responsiveness/smoothness | Shorter for faster signals in active markets, longer for stable trends in ranging markets |
|
||||
| Source | Close | Data point used for calculation | Change to HL2 or HLC3 for more balanced price representation |
|
||||
| Alpha | 2/(length+1) | Determines weighting decay | Direct alpha manipulation allows for precise tuning beyond standard length settings |
|
||||
|
||||
**Pro Tip:** Many professional traders use multiple EMAs simultaneously (e.g., 8, 21, 50) to identify potential support/resistance levels and trend strength based on their relative positioning.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
EMA works by calculating a weighted average where recent prices have more influence. The implementation uses an optimized form of the EMA calculation that is both computationally efficient and numerically stable.
|
||||
|
||||
**Technical formula:**
|
||||
The optimized EMA formula used in the implementation is:
|
||||
$$EMA_t = \alpha \cdot P_t + (1 - \alpha) \cdot EMA_{t-1}$$
|
||||
|
||||
Where:
|
||||
|
||||
* $\alpha = \frac{2}{N + 1}$ is the smoothing factor ($N$ is the period)
|
||||
* $P_t$ is the current price value
|
||||
* $EMA_{t-1}$ is the previous period's EMA value
|
||||
|
||||
This form is algebraically equivalent to the traditional EMA formula but offers better computational efficiency and numerical stability.
|
||||
|
||||
> 🔍 **Technical Note:** The implementation uses a sophisticated warm-up compensation method that provides accurate EMA values from the first bar. The compensation works by tracking an error term that decays exponentially:
|
||||
> $$e_t = e_{t-1} \cdot (1 - \alpha)$$
|
||||
> $$Compensation = \frac{1}{1 - e_t}$$
|
||||
> $$EMA_{corrected} = Compensation \cdot EMA_{raw}$$
|
||||
> This compensation automatically adjusts during the warm-up phase and becomes negligible ($e \le 1e^{-10}$) once sufficient data has been processed, ensuring mathematically correct values throughout the entire data series without requiring a traditional warm-up period.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides two implementations: a standard scalar version and a SIMD-optimized vector version for high-performance scenarios.
|
||||
|
||||
### Single EMA (`Ema`)
|
||||
|
||||
The `Ema` class calculates a single exponential moving average.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with period 10
|
||||
var ema = new Ema(10);
|
||||
|
||||
// Or initialize with specific alpha
|
||||
var emaAlpha = new Ema(0.5);
|
||||
|
||||
// Streaming update
|
||||
TValue result = ema.Update(new TValue(time, price));
|
||||
Console.WriteLine($"Current EMA: {result.Value}");
|
||||
|
||||
// Access current value property
|
||||
Console.WriteLine($"Current Value: {ema.Value.Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries results = Ema.Calculate(source, 10);
|
||||
```
|
||||
|
||||
### Multi-Alpha EMA (`EmaVector`)
|
||||
|
||||
The `EmaVector` class is a SIMD-optimized implementation for calculating multiple EMAs with different periods on the same input series simultaneously. It leverages hardware intrinsics (AVX/SSE) for high performance.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with multiple periods
|
||||
int[] periods = { 9, 12, 26 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// Streaming update
|
||||
TValue[] results = emaVector.Update(new TValue(time, price));
|
||||
|
||||
// Access values
|
||||
Console.WriteLine($"EMA(9): {results[0].Value}");
|
||||
Console.WriteLine($"EMA(12): {results[1].Value}");
|
||||
Console.WriteLine($"EMA(26): {results[2].Value}");
|
||||
|
||||
// Batch calculation
|
||||
TSeries source = ...;
|
||||
TSeries[] seriesResults = emaVector.Calculate(source);
|
||||
```
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
* **O(1) Complexity:** The calculation time is constant regardless of the period length.
|
||||
* **SIMD Optimization:** `EmaVector` processes multiple periods in parallel using vector instructions, significantly reducing CPU cycles for multi-timeframe analysis.
|
||||
* **Zero Allocation:** The streaming `Update` method is designed to be allocation-free (excluding the return struct).
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
The EMA's primary value comes from its ability to identify trend direction and potential reversal points:
|
||||
|
||||
* When price is above EMA, the short-term trend is generally bullish
|
||||
* When price is below EMA, the short-term trend is generally bearish
|
||||
* When a shorter-period EMA crosses above a longer-period EMA, it often signals the beginning of an uptrend
|
||||
* When a shorter-period EMA crosses below a longer-period EMA, it often signals the beginning of a downtrend
|
||||
* The slope of the EMA indicates trend strength and momentum
|
||||
|
||||
EMAs work particularly well in trending markets but may generate false signals during sideways or choppy conditions. For optimal results, traders typically use EMA crossovers or EMA-price crossovers as part of a broader system that includes volume and momentum confirmation.
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
* **Market conditions:** Less effective in choppy, sideways markets where price constantly crosses the average
|
||||
* **Lag factor:** While less significant than SMA, EMA still exhibits some lag, especially with longer lookback periods
|
||||
* **False signals:** Can produce whipsaws during consolidation phases or range-bound conditions
|
||||
* **Parameter sensitivity:** Small changes in length or alpha can significantly alter behavior
|
||||
* **Complementary tools:** Should be used with momentum indicators (RSI, MACD) or volume indicators for confirmation
|
||||
|
||||
## References
|
||||
|
||||
1. Murphy, J.J. (1999). *Technical Analysis of the Financial Markets*. New York Institute of Finance.
|
||||
2. Kaufman, P. (2013). *Trading Systems and Methods*, 5th Edition. Wiley Trading.
|
||||
3. Ehlers, J. (2001). *Rocket Science for Traders*. John Wiley & Sons.
|
||||
|
||||
+138
-138
@@ -1,138 +1,138 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaVectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Initialization_WithPeriods_SetsCorrectAlphas()
|
||||
{
|
||||
int[] periods = { 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// We can't check private fields directly, but we can check results after 1 step
|
||||
// Alpha = 2 / (P + 1)
|
||||
// P=10 -> A=2/11
|
||||
// P=20 -> A=2/21
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.Now, 100.0));
|
||||
|
||||
// First value should be 100.0 due to compensation
|
||||
Assert.Equal(100.0, res[0].Value, 1e-9);
|
||||
Assert.Equal(100.0, res[1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Streaming_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 };
|
||||
var time = DateTime.Now;
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
var tVal = new TValue(time, val);
|
||||
var multiRes = emaVector.Update(tVal);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(tVal);
|
||||
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
|
||||
Assert.Equal(singleRes.Time, multiRes[i].Time);
|
||||
}
|
||||
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var multiRes = emaVector.Calculate(series);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(series);
|
||||
|
||||
Assert.Equal(singleRes.Count, multiRes[i].Count);
|
||||
for (int j = 0; j < len; j++)
|
||||
{
|
||||
Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesStreaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVectorBatch = new EmaVector(periods);
|
||||
var emaVectorStream = new EmaVector(periods);
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
// Batch calculation
|
||||
var batchRes = emaVectorBatch.Calculate(series);
|
||||
|
||||
// Streaming calculation
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var tVal = new TValue(new DateTime(t[i]), v[i]);
|
||||
var streamRes = emaVectorStream.Update(tVal);
|
||||
|
||||
for (int j = 0; j < periods.Length; j++)
|
||||
{
|
||||
Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
int[] periods = { 10 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.Now, 100.0));
|
||||
emaVector.Reset();
|
||||
|
||||
// After reset, next calculation should treat it as first value (warmup)
|
||||
var res = emaVector.Update(new TValue(DateTime.Now, 200.0));
|
||||
|
||||
Assert.Equal(200.0, res[0].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class EmaVectorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Initialization_WithPeriods_SetsCorrectAlphas()
|
||||
{
|
||||
int[] periods = { 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
// We can't check private fields directly, but we can check results after 1 step
|
||||
// Alpha = 2 / (P + 1)
|
||||
// P=10 -> A=2/11
|
||||
// P=20 -> A=2/21
|
||||
|
||||
var res = emaVector.Update(new TValue(DateTime.Now, 100.0));
|
||||
|
||||
// First value should be 100.0 due to compensation
|
||||
Assert.Equal(100.0, res[0].Value, 1e-9);
|
||||
Assert.Equal(100.0, res[1].Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Streaming_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
var values = new double[] { 10, 20, 30, 40, 50, 40, 30, 20, 10 };
|
||||
var time = DateTime.Now;
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
var tVal = new TValue(time, val);
|
||||
var multiRes = emaVector.Update(tVal);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(tVal);
|
||||
Assert.Equal(singleRes.Value, multiRes[i].Value, 1e-9);
|
||||
Assert.Equal(singleRes.Time, multiRes[i].Time);
|
||||
}
|
||||
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesSingleEma()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
var emaSingles = periods.Select(p => new Ema(p)).ToArray();
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
var multiRes = emaVector.Calculate(series);
|
||||
|
||||
for (int i = 0; i < periods.Length; i++)
|
||||
{
|
||||
var singleRes = emaSingles[i].Update(series);
|
||||
|
||||
Assert.Equal(singleRes.Count, multiRes[i].Count);
|
||||
for (int j = 0; j < len; j++)
|
||||
{
|
||||
Assert.Equal(singleRes.Values[j], multiRes[i].Values[j], 1e-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_Series_MatchesStreaming()
|
||||
{
|
||||
int[] periods = { 5, 10, 20 };
|
||||
var emaVectorBatch = new EmaVector(periods);
|
||||
var emaVectorStream = new EmaVector(periods);
|
||||
|
||||
int len = 100;
|
||||
var t = new System.Collections.Generic.List<long>(len);
|
||||
var v = new System.Collections.Generic.List<double>(len);
|
||||
var now = DateTime.Now;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
t.Add(now.AddMinutes(i).Ticks);
|
||||
v.Add(Math.Sin(i * 0.1) * 100);
|
||||
}
|
||||
|
||||
var series = new TSeries(t, v);
|
||||
|
||||
// Batch calculation
|
||||
var batchRes = emaVectorBatch.Calculate(series);
|
||||
|
||||
// Streaming calculation
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var tVal = new TValue(new DateTime(t[i]), v[i]);
|
||||
var streamRes = emaVectorStream.Update(tVal);
|
||||
|
||||
for (int j = 0; j < periods.Length; j++)
|
||||
{
|
||||
Assert.Equal(batchRes[j].Values[i], streamRes[j].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
int[] periods = { 10 };
|
||||
var emaVector = new EmaVector(periods);
|
||||
|
||||
emaVector.Update(new TValue(DateTime.Now, 100.0));
|
||||
emaVector.Reset();
|
||||
|
||||
// After reset, next calculation should treat it as first value (warmup)
|
||||
var res = emaVector.Update(new TValue(DateTime.Now, 200.0));
|
||||
|
||||
Assert.Equal(200.0, res[0].Value, 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
+283
-271
@@ -1,271 +1,283 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
|
||||
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
|
||||
/// </summary>
|
||||
public class EmaVector
|
||||
{
|
||||
private readonly double[] _alphas;
|
||||
private readonly double[] _emas;
|
||||
private readonly double[] _Es;
|
||||
private readonly int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA values for all periods.
|
||||
/// </summary>
|
||||
public TValue[] Values { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified periods.
|
||||
/// </summary>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
public EmaVector(int[] periods)
|
||||
{
|
||||
_count = periods.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (periods[i] <= 0) throw new ArgumentException("Period must be greater than 0", nameof(periods));
|
||||
_alphas[i] = 2.0 / (periods[i] + 1);
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified alphas.
|
||||
/// </summary>
|
||||
/// <param name="alphas">Array of alphas</param>
|
||||
public EmaVector(double[] alphas)
|
||||
{
|
||||
_count = alphas.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (alphas[i] <= 0 || alphas[i] > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alphas));
|
||||
_alphas[i] = alphas[i];
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAt(int index)
|
||||
{
|
||||
_emas[index] = 0.0;
|
||||
_Es[index] = 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all EMA states.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
ResetAt(i);
|
||||
}
|
||||
Array.Clear(Values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMAs with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <returns>Array of compensated EMA values</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue[] Update(TValue input)
|
||||
{
|
||||
double val = input.Value;
|
||||
|
||||
// SIMD Loop
|
||||
int vecCount = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
var vecInput = new Vector<double>(val);
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
// Load state
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
// Update EMA
|
||||
// ema += alpha * (input - ema)
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
|
||||
// Update E (warmup factor)
|
||||
// E *= (1 - alpha)
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
// Calculate compensated result
|
||||
// res = ema / (1 - E)
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
|
||||
// Check warmup condition: E > 1e-10
|
||||
var warmupMask = Vector.GreaterThan(vecE, vecEpsilon);
|
||||
|
||||
// Select result
|
||||
// Vector.ConditionalSelect requires Vector<T> mask.
|
||||
// Vector.GreaterThan returns Vector<long> for double.
|
||||
// We cast Vector<long> to Vector<double> to use as mask.
|
||||
var vecResult = Vector.ConditionalSelect(Vector.AsVectorDouble(warmupMask), vecCompensated, vecEma);
|
||||
|
||||
// Store state
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Store result
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
Values[i + j] = new TValue(input.Time, vecResult[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining items
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Values[i] = new TValue(input.Time, result);
|
||||
}
|
||||
|
||||
return Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public TSeries[] Calculate(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var resultSeries = new TSeries[_count];
|
||||
|
||||
// Pre-allocate lists
|
||||
var tLists = new List<long>[_count];
|
||||
var vLists = new List<double>[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
tLists[i] = new List<long>(len);
|
||||
vLists[i] = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(tLists[i], len);
|
||||
CollectionsMarshal.SetCount(vLists[i], len);
|
||||
}
|
||||
|
||||
var sourceValues = source.Values;
|
||||
var sourceTimes = source.Times;
|
||||
|
||||
int vecCount = Vector<double>.Count;
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (int t = 0; t < len; t++)
|
||||
{
|
||||
double val = sourceValues[t];
|
||||
long time = sourceTimes[t];
|
||||
var vecInput = new Vector<double>(val);
|
||||
|
||||
int i = 0;
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
var warmupMask = Vector.GreaterThan(vecE, vecEpsilon);
|
||||
var vecResult = Vector.ConditionalSelect(Vector.AsVectorDouble(warmupMask), vecCompensated, vecEma);
|
||||
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Scatter results to lists
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
CollectionsMarshal.AsSpan(tLists[i + j])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i + j])[t] = vecResult[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Create TSeries and update Values
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
|
||||
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
|
||||
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
|
||||
Values[i] = new TValue(lastT, lastV);
|
||||
}
|
||||
|
||||
return resultSeries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series using specified periods.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public static TSeries[] Calculate(TSeries source, int[] periods)
|
||||
{
|
||||
var emaVector = new EmaVector(periods);
|
||||
return emaVector.Calculate(source);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Multi-Alpha Exponential Moving Average (EMA) - SIMD optimized.
|
||||
/// Calculates multiple EMAs with different periods/alphas for the same input series in parallel.
|
||||
/// </summary>
|
||||
public class EmaVector
|
||||
{
|
||||
private readonly double[] _alphas;
|
||||
private readonly double[] _emas;
|
||||
private readonly double[] _Es;
|
||||
private readonly int _count;
|
||||
|
||||
/// <summary>
|
||||
/// Current EMA values for all periods.
|
||||
/// </summary>
|
||||
public TValue[] Values { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified periods.
|
||||
/// </summary>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
public EmaVector(int[] periods)
|
||||
{
|
||||
_count = periods.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (periods[i] <= 0) throw new ArgumentException("Period must be greater than 0", nameof(periods));
|
||||
_alphas[i] = 2.0 / (periods[i] + 1);
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes EmaVector with specified alphas.
|
||||
/// </summary>
|
||||
/// <param name="alphas">Array of alphas</param>
|
||||
public EmaVector(double[] alphas)
|
||||
{
|
||||
_count = alphas.Length;
|
||||
_alphas = new double[_count];
|
||||
_emas = new double[_count];
|
||||
_Es = new double[_count];
|
||||
Values = new TValue[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
if (alphas[i] <= 0 || alphas[i] > 1) throw new ArgumentException("Alpha must be between 0 and 1", nameof(alphas));
|
||||
_alphas[i] = alphas[i];
|
||||
ResetAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetAt(int index)
|
||||
{
|
||||
_emas[index] = 0.0;
|
||||
_Es[index] = 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all EMA states.
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
ResetAt(i);
|
||||
}
|
||||
Array.Clear(Values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates EMAs with the given value.
|
||||
/// </summary>
|
||||
/// <param name="input">Input value</param>
|
||||
/// <returns>Array of compensated EMA values</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue[] Update(TValue input)
|
||||
{
|
||||
double val = input.Value;
|
||||
|
||||
// SIMD Loop
|
||||
int vecCount = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
var vecInput = new Vector<double>(val);
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
// Load state
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
// Update EMA
|
||||
// ema += alpha * (input - ema)
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
|
||||
// Update E (warmup factor)
|
||||
// E *= (1 - alpha)
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
// Calculate compensated result
|
||||
// res = ema / (1 - E)
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
|
||||
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
|
||||
// Vector.LessThanOrEqual returns Vector<long> with all-1s for true, all-0s for false
|
||||
// We reinterpret as Vector<double> for use with ConditionalSelect
|
||||
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
|
||||
|
||||
// Select result: if hot (E <= epsilon), use raw EMA; otherwise use compensated
|
||||
// ConditionalSelect: mask=true -> first arg, mask=false -> second arg
|
||||
var vecResult = Vector.ConditionalSelect(
|
||||
Vector.AsVectorDouble(isHotMask),
|
||||
vecEma, // Hot: use raw EMA
|
||||
vecCompensated // Cold: use compensated
|
||||
);
|
||||
|
||||
// Store state
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Store result
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
Values[i + j] = new TValue(input.Time, vecResult[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining items
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
Values[i] = new TValue(input.Time, result);
|
||||
}
|
||||
|
||||
return Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public TSeries[] Calculate(TSeries source)
|
||||
{
|
||||
int len = source.Count;
|
||||
var resultSeries = new TSeries[_count];
|
||||
|
||||
// Pre-allocate lists
|
||||
var tLists = new List<long>[_count];
|
||||
var vLists = new List<double>[_count];
|
||||
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
tLists[i] = new List<long>(len);
|
||||
vLists[i] = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(tLists[i], len);
|
||||
CollectionsMarshal.SetCount(vLists[i], len);
|
||||
}
|
||||
|
||||
var sourceValues = source.Values;
|
||||
var sourceTimes = source.Times;
|
||||
|
||||
int vecCount = Vector<double>.Count;
|
||||
var vecOne = Vector<double>.One;
|
||||
var vecEpsilon = new Vector<double>(1e-10);
|
||||
|
||||
for (int t = 0; t < len; t++)
|
||||
{
|
||||
double val = sourceValues[t];
|
||||
long time = sourceTimes[t];
|
||||
var vecInput = new Vector<double>(val);
|
||||
|
||||
int i = 0;
|
||||
if (Vector.IsHardwareAccelerated && _count >= vecCount)
|
||||
{
|
||||
for (; i <= _count - vecCount; i += vecCount)
|
||||
{
|
||||
var vecAlpha = new Vector<double>(_alphas, i);
|
||||
var vecEma = new Vector<double>(_emas, i);
|
||||
var vecE = new Vector<double>(_Es, i);
|
||||
|
||||
vecEma += vecAlpha * (vecInput - vecEma);
|
||||
vecE *= (vecOne - vecAlpha);
|
||||
|
||||
var vecCompensated = vecEma / (vecOne - vecE);
|
||||
|
||||
// Check warmup condition: E <= 1e-10 means "hot" (use raw EMA)
|
||||
var isHotMask = Vector.LessThanOrEqual(vecE, vecEpsilon);
|
||||
|
||||
// Select result: if hot, use raw EMA; otherwise use compensated
|
||||
var vecResult = Vector.ConditionalSelect(
|
||||
Vector.AsVectorDouble(isHotMask),
|
||||
vecEma, // Hot: use raw EMA
|
||||
vecCompensated // Cold: use compensated
|
||||
);
|
||||
|
||||
vecEma.CopyTo(_emas, i);
|
||||
vecE.CopyTo(_Es, i);
|
||||
|
||||
// Scatter results to lists
|
||||
for (int j = 0; j < vecCount; j++)
|
||||
{
|
||||
CollectionsMarshal.AsSpan(tLists[i + j])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i + j])[t] = vecResult[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (; i < _count; i++)
|
||||
{
|
||||
double alpha = _alphas[i];
|
||||
_emas[i] += alpha * (val - _emas[i]);
|
||||
|
||||
double result = _emas[i];
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
_Es[i] *= (1.0 - alpha);
|
||||
if (_Es[i] > 1e-10)
|
||||
{
|
||||
result = _emas[i] / (1.0 - _Es[i]);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionsMarshal.AsSpan(tLists[i])[t] = time;
|
||||
CollectionsMarshal.AsSpan(vLists[i])[t] = result;
|
||||
}
|
||||
}
|
||||
|
||||
// Create TSeries and update Values
|
||||
for (int i = 0; i < _count; i++)
|
||||
{
|
||||
resultSeries[i] = new TSeries(tLists[i], vLists[i]);
|
||||
var lastT = CollectionsMarshal.AsSpan(tLists[i])[len - 1];
|
||||
var lastV = CollectionsMarshal.AsSpan(vLists[i])[len - 1];
|
||||
Values[i] = new TValue(lastT, lastV);
|
||||
}
|
||||
|
||||
return resultSeries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates EMAs for the entire series using specified periods.
|
||||
/// </summary>
|
||||
/// <param name="source">Input series</param>
|
||||
/// <param name="periods">Array of periods</param>
|
||||
/// <returns>Array of EMA series</returns>
|
||||
public static TSeries[] Calculate(TSeries source, int[] periods)
|
||||
{
|
||||
var emaVector = new EmaVector(periods);
|
||||
return emaVector.Calculate(source);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,249 +1,249 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SimdExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.Equal(0.0, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_MultipleElements_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(55.0, span.SumSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_LargeArray_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = new double[1000];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i] = i + 1.0;
|
||||
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
double expected = 1000.0 * 1001.0 / 2.0; // Sum of 1..1000
|
||||
Assert.Equal(expected, span.SumSIMD(), precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MinSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_MultipleElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MaxSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_MultipleElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_MultipleElements_ReturnsCorrectAverage()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(3.0, span.AverageSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected variance: 4.571428... (sample variance)
|
||||
double variance = span.VarianceSIMD();
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected std dev: sqrt(4.571428) ≈ 2.138
|
||||
double stdDev = span.StdDevSIMD();
|
||||
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_EmptySpan_ReturnsBothNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.True(double.IsNaN(min));
|
||||
Assert.True(double.IsNaN(max));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_SingleElement_ReturnsSameValue()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(9.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTSeriesValues()
|
||||
{
|
||||
var series = new TSeries(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
|
||||
}
|
||||
|
||||
var values = series.Values;
|
||||
|
||||
double sum = values.SumSIMD();
|
||||
double avg = values.AverageSIMD();
|
||||
double min = values.MinSIMD();
|
||||
double max = values.MaxSIMD();
|
||||
var (minAlt, maxAlt) = values.MinMaxSIMD();
|
||||
|
||||
Assert.Equal(5050.0, sum, precision: 8); // Sum of 1..100
|
||||
Assert.Equal(50.5, avg, precision: 8);
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(100.0, max);
|
||||
Assert.Equal(min, minAlt);
|
||||
Assert.Equal(max, maxAlt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTBarSeriesClose()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_PerformanceTest_LargeDataset()
|
||||
{
|
||||
// Generate large dataset
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(10000, startTime, interval);
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
// Warm up
|
||||
_ = closeValues.SumSIMD();
|
||||
|
||||
// Test SIMD operations
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
|
||||
double variance = closeValues.VarianceSIMD();
|
||||
double stdDev = closeValues.StdDevSIMD();
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Verify results are valid
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
// Performance should be sub-millisecond for 10k elements
|
||||
Assert.True(sw.ElapsedMilliseconds < 10,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms");
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SimdExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void SumSIMD_EmptySpan_ReturnsZero()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.Equal(0.0, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.SumSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_MultipleElements_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(55.0, span.SumSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SumSIMD_LargeArray_ReturnsCorrectSum()
|
||||
{
|
||||
double[] data = new double[1000];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i] = i + 1.0;
|
||||
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
double expected = 1000.0 * 1001.0 / 2.0; // Sum of 1..1000
|
||||
Assert.Equal(expected, span.SumSIMD(), precision: 8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MinSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinSIMD_MultipleElements_ReturnsMinimum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(1.0, span.MinSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.MaxSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_SingleElement_ReturnsElement()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(42.5, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MaxSIMD_MultipleElements_ReturnsMaximum()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(9.0, span.MaxSIMD());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_EmptySpan_ReturnsNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
Assert.True(double.IsNaN(span.AverageSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AverageSIMD_MultipleElements_ReturnsCorrectAverage()
|
||||
{
|
||||
double[] data = [1.0, 2.0, 3.0, 4.0, 5.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.Equal(3.0, span.AverageSIMD(), precision: 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VarianceSIMD_MultipleElements_ReturnsCorrectVariance()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected variance: 4.571428... (sample variance)
|
||||
double variance = span.VarianceSIMD();
|
||||
Assert.True(Math.Abs(variance - 4.571428) < 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StdDevSIMD_MultipleElements_ReturnsCorrectStdDev()
|
||||
{
|
||||
double[] data = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
|
||||
// Expected std dev: sqrt(4.571428) ≈ 2.138
|
||||
double stdDev = span.StdDevSIMD();
|
||||
Assert.True(Math.Abs(stdDev - 2.138) < 0.01);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_EmptySpan_ReturnsBothNaN()
|
||||
{
|
||||
var span = ReadOnlySpan<double>.Empty;
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.True(double.IsNaN(min));
|
||||
Assert.True(double.IsNaN(max));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_SingleElement_ReturnsSameValue()
|
||||
{
|
||||
double[] data = [42.5];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(42.5, min);
|
||||
Assert.Equal(42.5, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinMaxSIMD_MultipleElements_ReturnsCorrectMinMax()
|
||||
{
|
||||
double[] data = [5.0, 2.0, 8.0, 1.0, 9.0, 3.0, 7.0, 4.0];
|
||||
var span = new ReadOnlySpan<double>(data);
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(9.0, max);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTSeriesValues()
|
||||
{
|
||||
var series = new TSeries(100);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow.Ticks + i, i + 1.0);
|
||||
}
|
||||
|
||||
var values = series.Values;
|
||||
|
||||
double sum = values.SumSIMD();
|
||||
double avg = values.AverageSIMD();
|
||||
double min = values.MinSIMD();
|
||||
double max = values.MaxSIMD();
|
||||
var (minAlt, maxAlt) = values.MinMaxSIMD();
|
||||
|
||||
Assert.Equal(5050.0, sum, precision: 8); // Sum of 1..100
|
||||
Assert.Equal(50.5, avg, precision: 8);
|
||||
Assert.Equal(1.0, min);
|
||||
Assert.Equal(100.0, max);
|
||||
Assert.Equal(min, minAlt);
|
||||
Assert.Equal(max, maxAlt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_WorksWithTBarSeriesClose()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SIMD_PerformanceTest_LargeDataset()
|
||||
{
|
||||
// Generate large dataset
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var bars = gbm.Fetch(10000, startTime, interval);
|
||||
var closeValues = bars.Close.Values;
|
||||
|
||||
// Warm up
|
||||
_ = closeValues.SumSIMD();
|
||||
|
||||
// Test SIMD operations
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
|
||||
double sum = closeValues.SumSIMD();
|
||||
double avg = closeValues.AverageSIMD();
|
||||
double min = closeValues.MinSIMD();
|
||||
double max = closeValues.MaxSIMD();
|
||||
var (minAlt, maxAlt) = closeValues.MinMaxSIMD();
|
||||
double variance = closeValues.VarianceSIMD();
|
||||
double stdDev = closeValues.StdDevSIMD();
|
||||
|
||||
sw.Stop();
|
||||
|
||||
// Verify results are valid
|
||||
Assert.True(sum > 0);
|
||||
Assert.True(avg > 0);
|
||||
Assert.True(min > 0);
|
||||
Assert.True(max > min);
|
||||
Assert.True(variance > 0);
|
||||
Assert.True(stdDev > 0);
|
||||
|
||||
// Performance should be sub-millisecond for 10k elements
|
||||
Assert.True(sw.ElapsedMilliseconds < 10,
|
||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 10ms");
|
||||
}
|
||||
}
|
||||
|
||||
+280
-280
@@ -1,280 +1,280 @@
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SIMD-accelerated extension methods for high-performance array operations.
|
||||
/// Uses Vector<T> for 4-8x speedup on supported hardware with automatic scalar fallback.
|
||||
/// </summary>
|
||||
public static class SimdExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates sum using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double SumSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return 0.0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
Vector<double> sum = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
sum += vector;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sum[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
result += span[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double scalar = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
scalar += span[i];
|
||||
return scalar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates minimum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MinSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
}
|
||||
|
||||
// Find minimum within vector
|
||||
double result = minVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < result)
|
||||
result = minVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double min = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min)
|
||||
min = span[i];
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates maximum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var maxVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find maximum within vector
|
||||
double result = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (maxVec[j] > result)
|
||||
result = maxVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double max = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max)
|
||||
max = span[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates average using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
return span.SumSIMD() / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
||||
/// More numerically stable than naive two-pass algorithm.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
|
||||
double m = mean ?? span.AverageSIMD();
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
var meanVec = new Vector<double>(m);
|
||||
Vector<double> sumSq = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
var diff = vector - meanVec;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sumSq[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
result += diff * diff;
|
||||
}
|
||||
|
||||
return result / (span.Length - 1);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double sumSquares = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
sumSquares += diff * diff;
|
||||
}
|
||||
return sumSquares / (span.Length - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates standard deviation using SIMD vectorization.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
return Math.Sqrt(span.VarianceSIMD(mean));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds both min and max in a single pass using SIMD vectorization.
|
||||
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return (double.NaN, double.NaN);
|
||||
if (span.Length == 1) return (span[0], span[0]);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
var maxVec = minVec;
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find min/max within vectors
|
||||
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];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min) min = span[i];
|
||||
if (span[i] > max) max = span[i];
|
||||
}
|
||||
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
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);
|
||||
}
|
||||
}
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SIMD-accelerated extension methods for high-performance array operations.
|
||||
/// Uses Vector<T> for 4-8x speedup on supported hardware with automatic scalar fallback.
|
||||
/// </summary>
|
||||
public static class SimdExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates sum using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double SumSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return 0.0;
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
Vector<double> sum = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
sum += vector;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sum[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
result += span[i];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double scalar = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
scalar += span[i];
|
||||
return scalar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates minimum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MinSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
}
|
||||
|
||||
// Find minimum within vector
|
||||
double result = minVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (minVec[j] < result)
|
||||
result = minVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double min = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min)
|
||||
min = span[i];
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates maximum value using SIMD vectorization when available.
|
||||
/// 4-6x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double MaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
if (span.Length == 1) return span[0];
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var maxVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find maximum within vector
|
||||
double result = maxVec[0];
|
||||
for (int j = 1; j < vectorSize; j++)
|
||||
{
|
||||
if (maxVec[j] > result)
|
||||
result = maxVec[j];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > result)
|
||||
result = span[i];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double max = span[0];
|
||||
for (int i = 1; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] > max)
|
||||
max = span[i];
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates average using SIMD vectorization when available.
|
||||
/// 4-8x faster than scalar loop on AVX2/AVX-512 hardware.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double AverageSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return double.NaN;
|
||||
return span.SumSIMD() / span.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates variance using SIMD vectorization (Welford's online algorithm adapted).
|
||||
/// More numerically stable than naive two-pass algorithm.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
if (span.Length < 2) return double.NaN;
|
||||
|
||||
double m = mean ?? span.AverageSIMD();
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
var meanVec = new Vector<double>(m);
|
||||
Vector<double> sumSq = Vector<double>.Zero;
|
||||
int vectorSize = Vector<double>.Count;
|
||||
int i = 0;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
var diff = vector - meanVec;
|
||||
sumSq += diff * diff;
|
||||
}
|
||||
|
||||
// Horizontal sum of vector
|
||||
double result = 0.0;
|
||||
for (int j = 0; j < vectorSize; j++)
|
||||
result += sumSq[j];
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
result += diff * diff;
|
||||
}
|
||||
|
||||
return result / (span.Length - 1);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
double sumSquares = 0.0;
|
||||
for (int i = 0; i < span.Length; i++)
|
||||
{
|
||||
double diff = span[i] - m;
|
||||
sumSquares += diff * diff;
|
||||
}
|
||||
return sumSquares / (span.Length - 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates standard deviation using SIMD vectorization.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double StdDevSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||
{
|
||||
return Math.Sqrt(span.VarianceSIMD(mean));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds both min and max in a single pass using SIMD vectorization.
|
||||
/// More efficient than calling MinSIMD and MaxSIMD separately.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static (double Min, double Max) MinMaxSIMD(this ReadOnlySpan<double> span)
|
||||
{
|
||||
if (span.IsEmpty) return (double.NaN, double.NaN);
|
||||
if (span.Length == 1) return (span[0], span[0]);
|
||||
|
||||
if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
|
||||
{
|
||||
int vectorSize = Vector<double>.Count;
|
||||
var minVec = new Vector<double>(span.Slice(0, vectorSize));
|
||||
var maxVec = minVec;
|
||||
int i = vectorSize;
|
||||
|
||||
// Process in vector chunks
|
||||
for (; i <= span.Length - vectorSize; i += vectorSize)
|
||||
{
|
||||
var vector = new Vector<double>(span.Slice(i, vectorSize));
|
||||
minVec = Vector.Min(minVec, vector);
|
||||
maxVec = Vector.Max(maxVec, vector);
|
||||
}
|
||||
|
||||
// Find min/max within vectors
|
||||
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];
|
||||
}
|
||||
|
||||
// Process remaining elements
|
||||
for (; i < span.Length; i++)
|
||||
{
|
||||
if (span[i] < min) min = span[i];
|
||||
if (span[i] > max) max = span[i];
|
||||
}
|
||||
|
||||
return (min, max);
|
||||
}
|
||||
|
||||
// Scalar fallback
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
# SimdExtensions Class
|
||||
|
||||
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
|
||||
- **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
|
||||
- **Zero-Allocation**: Operates directly on spans without creating new arrays.
|
||||
- **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
|
||||
|
||||
## Available Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `SumSIMD()` | Calculates the sum of elements. |
|
||||
| `MinSIMD()` | Finds the minimum value. |
|
||||
| `MaxSIMD()` | Finds the maximum value. |
|
||||
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
|
||||
| `AverageSIMD()` | Calculates the arithmetic mean. |
|
||||
| `VarianceSIMD()` | Calculates the sample variance. |
|
||||
| `StdDevSIMD()` | Calculates the sample standard deviation. |
|
||||
|
||||
## Performance
|
||||
|
||||
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
|
||||
ReadOnlySpan<double> span = data;
|
||||
|
||||
// Calculate sum
|
||||
double sum = span.SumSIMD();
|
||||
|
||||
// Calculate min and max in one pass
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
|
||||
// Calculate standard deviation
|
||||
double stdDev = span.StdDevSIMD();
|
||||
# SimdExtensions Class
|
||||
|
||||
`SimdExtensions` provides high-performance, SIMD-accelerated extension methods for `ReadOnlySpan<double>`. It leverages .NET's `Vector<T>` to achieve 4-8x speedups on supported hardware (AVX2, AVX-512) while automatically falling back to scalar implementations on older hardware.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Hardware Acceleration**: Uses CPU vector registers to process multiple elements in parallel.
|
||||
- **Automatic Fallback**: Gracefully handles non-SIMD hardware or small arrays.
|
||||
- **Zero-Allocation**: Operates directly on spans without creating new arrays.
|
||||
- **Aggressive Inlining**: Methods are marked for inlining to minimize call overhead.
|
||||
|
||||
## Available Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `SumSIMD()` | Calculates the sum of elements. |
|
||||
| `MinSIMD()` | Finds the minimum value. |
|
||||
| `MaxSIMD()` | Finds the maximum value. |
|
||||
| `MinMaxSIMD()` | Finds both min and max in a single pass (more efficient than separate calls). |
|
||||
| `AverageSIMD()` | Calculates the arithmetic mean. |
|
||||
| `VarianceSIMD()` | Calculates the sample variance. |
|
||||
| `StdDevSIMD()` | Calculates the sample standard deviation. |
|
||||
|
||||
## Performance
|
||||
|
||||
On modern CPUs (e.g., Intel Core i7/i9, AMD Ryzen), these methods typically outperform standard LINQ or scalar loops by a factor of 4 to 8 for large arrays.
|
||||
|
||||
## Usage
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
double[] data = { 1.0, 2.0, 3.0, 4.0, 5.0, ... };
|
||||
ReadOnlySpan<double> span = data;
|
||||
|
||||
// Calculate sum
|
||||
double sum = span.SumSIMD();
|
||||
|
||||
// Calculate min and max in one pass
|
||||
var (min, max) = span.MinMaxSIMD();
|
||||
|
||||
// Calculate standard deviation
|
||||
double stdDev = span.StdDevSIMD();
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBar
|
||||
// TBar represents a single OHLCV bar (Open, High, Low, Close, Volume)
|
||||
// It is an immutable struct optimized for memory and performance
|
||||
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
|
||||
Console.WriteLine($"Created TBar: {bar}");
|
||||
Console.WriteLine($"Time: {bar.AsDateTime}");
|
||||
Console.WriteLine($"Open: {bar.Open}");
|
||||
Console.WriteLine($"High: {bar.High}");
|
||||
Console.WriteLine($"Low: {bar.Low}");
|
||||
Console.WriteLine($"Close: {bar.Close}");
|
||||
Console.WriteLine($"Volume: {bar.Volume}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Computed Properties
|
||||
// TBar provides on-demand calculation of common price averages
|
||||
// These are calculated when accessed, saving storage space
|
||||
|
||||
Console.WriteLine($"HL2 (High+Low)/2: {bar.HL2}");
|
||||
Console.WriteLine($"OC2 (Open+Close)/2: {bar.OC2}");
|
||||
Console.WriteLine($"OHL3 (Open+High+Low)/3: {bar.OHL3}");
|
||||
Console.WriteLine($"HLC3 (High+Low+Close)/3: {bar.HLC3}");
|
||||
Console.WriteLine($"OHLC4 (Open+High+Low+Close)/4: {bar.OHLC4}");
|
||||
Console.WriteLine($"HLCC4 (High+Low+Close+Close)/4: {bar.HLCC4}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. TValue Accessors
|
||||
// You can efficiently access individual components as TValue (Time-Value pair)
|
||||
// This is useful when you need to treat a specific price component as a time series point
|
||||
|
||||
Console.WriteLine($"Open TValue: {bar.O}");
|
||||
Console.WriteLine($"High TValue: {bar.H}");
|
||||
Console.WriteLine($"Low TValue: {bar.L}");
|
||||
Console.WriteLine($"Close TValue: {bar.C}");
|
||||
Console.WriteLine($"Volume TValue: {bar.V}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Implicit Conversions
|
||||
// TBar supports implicit conversions to double (Close price), TValue (Close), and DateTime
|
||||
|
||||
double closePrice = bar;
|
||||
TValue value = bar;
|
||||
DateTime dt = bar;
|
||||
|
||||
Console.WriteLine($"Implicit double (Close): {closePrice}");
|
||||
Console.WriteLine($"Implicit TValue (Close): {value}");
|
||||
Console.WriteLine($"Implicit DateTime: {dt}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Equality and Immutability
|
||||
// Being a struct, TBar has value semantics
|
||||
|
||||
var bar2 = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
var bar3 = new TBar(now, 101.0, 106.0, 96.0, 103.0, 1100.0);
|
||||
|
||||
Console.WriteLine($"bar equals bar2? {bar == bar2}"); // True, same values
|
||||
Console.WriteLine($"bar equals bar3? {bar == bar3}"); // False, different values
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBar
|
||||
// TBar represents a single OHLCV bar (Open, High, Low, Close, Volume)
|
||||
// It is an immutable struct optimized for memory and performance
|
||||
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
|
||||
Console.WriteLine($"Created TBar: {bar}");
|
||||
Console.WriteLine($"Time: {bar.AsDateTime}");
|
||||
Console.WriteLine($"Open: {bar.Open}");
|
||||
Console.WriteLine($"High: {bar.High}");
|
||||
Console.WriteLine($"Low: {bar.Low}");
|
||||
Console.WriteLine($"Close: {bar.Close}");
|
||||
Console.WriteLine($"Volume: {bar.Volume}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Computed Properties
|
||||
// TBar provides on-demand calculation of common price averages
|
||||
// These are calculated when accessed, saving storage space
|
||||
|
||||
Console.WriteLine($"HL2 (High+Low)/2: {bar.HL2}");
|
||||
Console.WriteLine($"OC2 (Open+Close)/2: {bar.OC2}");
|
||||
Console.WriteLine($"OHL3 (Open+High+Low)/3: {bar.OHL3}");
|
||||
Console.WriteLine($"HLC3 (High+Low+Close)/3: {bar.HLC3}");
|
||||
Console.WriteLine($"OHLC4 (Open+High+Low+Close)/4: {bar.OHLC4}");
|
||||
Console.WriteLine($"HLCC4 (High+Low+Close+Close)/4: {bar.HLCC4}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. TValue Accessors
|
||||
// You can efficiently access individual components as TValue (Time-Value pair)
|
||||
// This is useful when you need to treat a specific price component as a time series point
|
||||
|
||||
Console.WriteLine($"Open TValue: {bar.O}");
|
||||
Console.WriteLine($"High TValue: {bar.H}");
|
||||
Console.WriteLine($"Low TValue: {bar.L}");
|
||||
Console.WriteLine($"Close TValue: {bar.C}");
|
||||
Console.WriteLine($"Volume TValue: {bar.V}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Implicit Conversions
|
||||
// TBar supports implicit conversions to double (Close price), TValue (Close), and DateTime
|
||||
|
||||
double closePrice = bar;
|
||||
TValue value = bar;
|
||||
DateTime dt = bar;
|
||||
|
||||
Console.WriteLine($"Implicit double (Close): {closePrice}");
|
||||
Console.WriteLine($"Implicit TValue (Close): {value}");
|
||||
Console.WriteLine($"Implicit DateTime: {dt}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Equality and Immutability
|
||||
// Being a struct, TBar has value semantics
|
||||
|
||||
var bar2 = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
var bar3 = new TBar(now, 101.0, 106.0, 96.0, 103.0, 1100.0);
|
||||
|
||||
Console.WriteLine($"bar equals bar2? {bar == bar2}"); // True, same values
|
||||
Console.WriteLine($"bar equals bar3? {bar == bar3}"); // False, different values
|
||||
|
||||
+76
-76
@@ -1,76 +1,76 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double open = 100;
|
||||
double high = 110;
|
||||
double low = 90;
|
||||
double close = 105;
|
||||
double volume = 1000;
|
||||
|
||||
var bar = new TBar(time, open, high, low, close, volume);
|
||||
|
||||
Assert.Equal(time, bar.Time);
|
||||
Assert.Equal(open, bar.Open);
|
||||
Assert.Equal(high, bar.High);
|
||||
Assert.Equal(low, bar.Low);
|
||||
Assert.Equal(close, bar.Close);
|
||||
Assert.Equal(volume, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.HL2); // (110 + 90) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.OHL3); // (100 + 110 + 90) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLC3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLC3); // (110 + 90 + 100) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.OHLC4); // (100 + 110 + 90 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLCC4); // (110 + 90 + 100 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.0, tv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double open = 100;
|
||||
double high = 110;
|
||||
double low = 90;
|
||||
double close = 105;
|
||||
double volume = 1000;
|
||||
|
||||
var bar = new TBar(time, open, high, low, close, volume);
|
||||
|
||||
Assert.Equal(time, bar.Time);
|
||||
Assert.Equal(open, bar.Open);
|
||||
Assert.Equal(high, bar.High);
|
||||
Assert.Equal(low, bar.Low);
|
||||
Assert.Equal(close, bar.Close);
|
||||
Assert.Equal(volume, bar.Volume);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HL2_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.HL2); // (110 + 90) / 2
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHL3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 105, 1000);
|
||||
Assert.Equal(100.0, bar.OHL3); // (100 + 110 + 90) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLC3_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLC3); // (110 + 90 + 100) / 3
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OHLC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.OHLC4); // (100 + 110 + 90 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HLCC4_CalculatesCorrectly()
|
||||
{
|
||||
var bar = new TBar(0, 100, 110, 90, 100, 1000);
|
||||
Assert.Equal(100.0, bar.HLCC4); // (110 + 90 + 100 + 100) / 4
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToTValue_ReturnsClosePriceWithTime()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
|
||||
TValue tv = bar;
|
||||
|
||||
Assert.Equal(time, tv.Time);
|
||||
Assert.Equal(105.0, tv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+68
-68
@@ -1,68 +1,68 @@
|
||||
# TBar Struct
|
||||
|
||||
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It is designed for high-performance financial data processing with minimal memory overhead.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Memory Efficient**: Pure data type occupying exactly 48 bytes (1 `long` + 5 `double`s).
|
||||
- **Immutable**: Thread-safe by design.
|
||||
- **Zero-Copy Conversions**: Efficiently converts to `TValue` for individual price components (Open, High, Low, Close, Volume).
|
||||
- **Computed Properties**: Provides on-demand calculation of common price averages (HL2, HLC3, etc.) without storage overhead.
|
||||
- **SIMD Compatible**: Layout is optimized for potential vectorization in collection types.
|
||||
|
||||
## Structure Definition
|
||||
|
||||
```csharp
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
public readonly long Time; // Unix ticks
|
||||
public readonly double Open;
|
||||
public readonly double High;
|
||||
public readonly double Low;
|
||||
public readonly double Close;
|
||||
public readonly double Volume;
|
||||
}
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Time` | `long` | Timestamp in ticks. |
|
||||
| `Open` | `double` | Opening price. |
|
||||
| `High` | `double` | Highest price. |
|
||||
| `Low` | `double` | Lowest price. |
|
||||
| `Close` | `double` | Closing price. |
|
||||
| `Volume` | `double` | Traded volume. |
|
||||
| `AsDateTime` | `DateTime` | `Time` converted to UTC DateTime. |
|
||||
|
||||
### Computed Averages
|
||||
These properties are calculated on the fly:
|
||||
- `HL2`: (High + Low) / 2
|
||||
- `OC2`: (Open + Close) / 2
|
||||
- `OHL3`: (Open + High + Low) / 3
|
||||
- `HLC3`: (High + Low + Close) / 3
|
||||
- `OHLC4`: (Open + High + Low + Close) / 4
|
||||
- `HLCC4`: (High + Low + Close + Close) / 4
|
||||
|
||||
### TValue Accessors
|
||||
Efficiently access components as `TValue` (Time-Value pair):
|
||||
- `O`: (Time, Open)
|
||||
- `H`: (Time, High)
|
||||
- `L`: (Time, Low)
|
||||
- `C`: (Time, Close)
|
||||
- `V`: (Time, Volume)
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a TBar
|
||||
```csharp
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
```
|
||||
|
||||
### Implicit Conversions
|
||||
```csharp
|
||||
double closePrice = bar; // Implicitly converts to Close price
|
||||
TValue value = bar; // Implicitly converts to (Time, Close)
|
||||
DateTime dt = bar; // Implicitly converts to DateTime
|
||||
# TBar Struct
|
||||
|
||||
`TBar` is a lightweight, immutable struct representing a single OHLCV (Open, High, Low, Close, Volume) bar. It is designed for high-performance financial data processing with minimal memory overhead.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Memory Efficient**: Pure data type occupying exactly 48 bytes (1 `long` + 5 `double`s).
|
||||
- **Immutable**: Thread-safe by design.
|
||||
- **Zero-Copy Conversions**: Efficiently converts to `TValue` for individual price components (Open, High, Low, Close, Volume).
|
||||
- **Computed Properties**: Provides on-demand calculation of common price averages (HL2, HLC3, etc.) without storage overhead.
|
||||
- **SIMD Compatible**: Layout is optimized for potential vectorization in collection types.
|
||||
|
||||
## Structure Definition
|
||||
|
||||
```csharp
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
public readonly long Time; // Unix ticks
|
||||
public readonly double Open;
|
||||
public readonly double High;
|
||||
public readonly double Low;
|
||||
public readonly double Close;
|
||||
public readonly double Volume;
|
||||
}
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `Time` | `long` | Timestamp in ticks. |
|
||||
| `Open` | `double` | Opening price. |
|
||||
| `High` | `double` | Highest price. |
|
||||
| `Low` | `double` | Lowest price. |
|
||||
| `Close` | `double` | Closing price. |
|
||||
| `Volume` | `double` | Traded volume. |
|
||||
| `AsDateTime` | `DateTime` | `Time` converted to UTC DateTime. |
|
||||
|
||||
### Computed Averages
|
||||
These properties are calculated on the fly:
|
||||
- `HL2`: (High + Low) / 2
|
||||
- `OC2`: (Open + Close) / 2
|
||||
- `OHL3`: (Open + High + Low) / 3
|
||||
- `HLC3`: (High + Low + Close) / 3
|
||||
- `OHLC4`: (Open + High + Low + Close) / 4
|
||||
- `HLCC4`: (High + Low + Close + Close) / 4
|
||||
|
||||
### TValue Accessors
|
||||
Efficiently access components as `TValue` (Time-Value pair):
|
||||
- `O`: (Time, Open)
|
||||
- `H`: (Time, High)
|
||||
- `L`: (Time, Low)
|
||||
- `C`: (Time, Close)
|
||||
- `V`: (Time, Volume)
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating a TBar
|
||||
```csharp
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
var bar = new TBar(now, 100.0, 105.0, 95.0, 102.0, 1000.0);
|
||||
```
|
||||
|
||||
### Implicit Conversions
|
||||
```csharp
|
||||
double closePrice = bar; // Implicitly converts to Close price
|
||||
TValue value = bar; // Implicitly converts to (Time, Close)
|
||||
DateTime dt = bar; // Implicitly converts to DateTime
|
||||
|
||||
+83
-83
@@ -1,83 +1,83 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing an OHLCV bar.
|
||||
/// Pure data type: 48 bytes (long + 5 doubles).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
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) / 3.0; }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing an OHLCV bar.
|
||||
/// Pure data type: 48 bytes (long + 5 doubles).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TBar : IEquatable<TBar>
|
||||
{
|
||||
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) / 3.0; }
|
||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,82 +1,82 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBarSeries
|
||||
// TBarSeries is a collection of bars stored in Structure of Arrays (SoA) format
|
||||
// This layout is optimized for performance and SIMD operations
|
||||
|
||||
var bars = new TBarSeries();
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Add a new bar
|
||||
var bar1 = new TBar(now, 100, 105, 95, 102, 1000);
|
||||
bars.Add(bar1, isNew: true);
|
||||
|
||||
Console.WriteLine($"Added Bar 1: Count={bars.Count}");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Streaming Updates
|
||||
// TBarSeries supports updating the last bar in place
|
||||
// This is crucial for real-time feeds where the current bar changes until it closes
|
||||
|
||||
// Update the bar (e.g. price changed within the same minute)
|
||||
var bar1Update = new TBar(now, 100, 106, 95, 104, 1500);
|
||||
bars.Add(bar1Update, isNew: false);
|
||||
|
||||
Console.WriteLine($"Updated Bar 1: Count={bars.Count} (Count should not increase)");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
Console.WriteLine($"Last High: {bars.Last.High}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. Zero-Copy Views
|
||||
// You can access individual components (Open, High, Low, Close, Volume) as TSeries
|
||||
// These views share the underlying memory, so no copying is involved
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count}");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value}");
|
||||
|
||||
// Verify view updates automatically
|
||||
Console.WriteLine("\nAdding new bar...");
|
||||
bars.Add(now + TimeSpan.TicksPerMinute, 104, 108, 103, 107, 2000, isNew: true);
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count} (Should match Bars Count)");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value} (Should be 107)");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Aliases and Direct Access
|
||||
// TBarSeries provides short aliases (O, H, L, C, V) and direct access properties
|
||||
|
||||
Console.WriteLine($"Alias Access (C.Last): {bars.C.Last.Value}");
|
||||
Console.WriteLine($"Direct Last Access (LastClose): {bars.LastClose}");
|
||||
Console.WriteLine($"Direct Last Time (LastTime): {new DateTime(bars.LastTime)}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Iteration
|
||||
// You can iterate over the bars or individual series
|
||||
|
||||
Console.WriteLine("\nIterating over bars:");
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
Console.WriteLine($" {bar}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\nIterating over Close prices:");
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Console.WriteLine($" Bar {i}: Close={bars.Close[i].Value}");
|
||||
}
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"aliases":[],"languageName":"csharp","name":"csharp"}]}}
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Creating a TBarSeries
|
||||
// TBarSeries is a collection of bars stored in Structure of Arrays (SoA) format
|
||||
// This layout is optimized for performance and SIMD operations
|
||||
|
||||
var bars = new TBarSeries();
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Add a new bar
|
||||
var bar1 = new TBar(now, 100, 105, 95, 102, 1000);
|
||||
bars.Add(bar1, isNew: true);
|
||||
|
||||
Console.WriteLine($"Added Bar 1: Count={bars.Count}");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 2. Streaming Updates
|
||||
// TBarSeries supports updating the last bar in place
|
||||
// This is crucial for real-time feeds where the current bar changes until it closes
|
||||
|
||||
// Update the bar (e.g. price changed within the same minute)
|
||||
var bar1Update = new TBar(now, 100, 106, 95, 104, 1500);
|
||||
bars.Add(bar1Update, isNew: false);
|
||||
|
||||
Console.WriteLine($"Updated Bar 1: Count={bars.Count} (Count should not increase)");
|
||||
Console.WriteLine($"Last Close: {bars.Last.Close}");
|
||||
Console.WriteLine($"Last High: {bars.Last.High}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 3. Zero-Copy Views
|
||||
// You can access individual components (Open, High, Low, Close, Volume) as TSeries
|
||||
// These views share the underlying memory, so no copying is involved
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count}");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value}");
|
||||
|
||||
// Verify view updates automatically
|
||||
Console.WriteLine("\nAdding new bar...");
|
||||
bars.Add(now + TimeSpan.TicksPerMinute, 104, 108, 103, 107, 2000, isNew: true);
|
||||
|
||||
Console.WriteLine($"Bars Count: {bars.Count}");
|
||||
Console.WriteLine($"Close Series Count: {bars.Close.Count} (Should match Bars Count)");
|
||||
Console.WriteLine($"Close Series Last: {bars.Close.Last.Value} (Should be 107)");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 4. Aliases and Direct Access
|
||||
// TBarSeries provides short aliases (O, H, L, C, V) and direct access properties
|
||||
|
||||
Console.WriteLine($"Alias Access (C.Last): {bars.C.Last.Value}");
|
||||
Console.WriteLine($"Direct Last Access (LastClose): {bars.LastClose}");
|
||||
Console.WriteLine($"Direct Last Time (LastTime): {new DateTime(bars.LastTime)}");
|
||||
|
||||
#!csharp
|
||||
|
||||
// 5. Iteration
|
||||
// You can iterate over the bars or individual series
|
||||
|
||||
Console.WriteLine("\nIterating over bars:");
|
||||
foreach (var bar in bars)
|
||||
{
|
||||
Console.WriteLine($" {bar}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\nIterating over Close prices:");
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
Console.WriteLine($" Bar {i}: Close={bars.Close[i].Value}");
|
||||
}
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewBar_IncreasesCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(105.0, series.Last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateBar_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar1 = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(time, 100, 112, 90, 108, 1200);
|
||||
|
||||
series.Add(bar1, isNew: true);
|
||||
series.Add(bar2, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(108.0, series.Last.Close);
|
||||
Assert.Equal(112.0, series.Last.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_AreUpdated()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series.Open);
|
||||
Assert.Single(series.High);
|
||||
Assert.Single(series.Low);
|
||||
Assert.Single(series.Close);
|
||||
Assert.Single(series.Volume);
|
||||
|
||||
Assert.Equal(100.0, series.Open.Last.Value);
|
||||
Assert.Equal(110.0, series.High.Last.Value);
|
||||
Assert.Equal(90.0, series.Low.Last.Value);
|
||||
Assert.Equal(105.0, series.Close.Last.Value);
|
||||
Assert.Equal(1000.0, series.Volume.Last.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TBarSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewBar_IncreasesCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(105.0, series.Last.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateBar_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
var bar1 = new TBar(time, 100, 110, 90, 105, 1000);
|
||||
var bar2 = new TBar(time, 100, 112, 90, 108, 1200);
|
||||
|
||||
series.Add(bar1, isNew: true);
|
||||
series.Add(bar2, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(108.0, series.Last.Close);
|
||||
Assert.Equal(112.0, series.Last.High);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SubSeries_AreUpdated()
|
||||
{
|
||||
var series = new TBarSeries();
|
||||
var bar = new TBar(DateTime.UtcNow.Ticks, 100, 110, 90, 105, 1000);
|
||||
|
||||
series.Add(bar, isNew: true);
|
||||
|
||||
Assert.Single(series.Open);
|
||||
Assert.Single(series.High);
|
||||
Assert.Single(series.Low);
|
||||
Assert.Single(series.Close);
|
||||
Assert.Single(series.Volume);
|
||||
|
||||
Assert.Equal(100.0, series.Open.Last.Value);
|
||||
Assert.Equal(110.0, series.High.Last.Value);
|
||||
Assert.Equal(90.0, series.Low.Last.Value);
|
||||
Assert.Equal(105.0, series.Close.Last.Value);
|
||||
Assert.Equal(1000.0, series.Volume.Last.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
# TBarSeries Class
|
||||
|
||||
`TBarSeries` is a high-performance collection of OHLCV bars implemented using a Structure of Arrays (SoA) layout. This design optimizes memory access patterns and enables efficient SIMD operations while providing convenient object-oriented views.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Structure of Arrays (SoA)**: Stores Time, Open, High, Low, Close, and Volume in separate contiguous arrays rather than an array of structs. This improves cache locality for operations that only need specific components (e.g., calculating SMA on Close prices).
|
||||
- **Zero-Copy Views**: Exposes `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`) that view the underlying data without copying.
|
||||
- **Streaming Support**: Efficiently handles real-time data updates with `Add(bar, isNew: false)`.
|
||||
- **Memory Efficient**: Minimizes object overhead by using shared internal lists.
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Views
|
||||
public TSeries Open { get; }
|
||||
public TSeries High { get; }
|
||||
public TSeries Low { get; }
|
||||
public TSeries Close { get; }
|
||||
public TSeries Volume { get; }
|
||||
|
||||
// Aliases
|
||||
public TSeries O => Open;
|
||||
public TSeries H => High;
|
||||
public TSeries L => Low;
|
||||
public TSeries C => Close;
|
||||
public TSeries V => Volume;
|
||||
}
|
||||
```
|
||||
|
||||
## Core Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `Add(TBar bar, bool isNew = true)` | Adds a new bar or updates the last one. |
|
||||
| `Add(DateTime time, double o, double h, double l, double c, double v, bool isNew)` | Adds raw values directly. |
|
||||
| `Count` | Returns the number of bars. |
|
||||
| `Last` | Returns the most recent `TBar`. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Populating
|
||||
```csharp
|
||||
var bars = new TBarSeries();
|
||||
|
||||
// Add a new bar
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
bars.Add(new TBar(now, 100, 105, 95, 102, 1000), isNew: true);
|
||||
|
||||
// Update the last bar (e.g., real-time feed update)
|
||||
bars.Add(new TBar(now, 100, 106, 95, 104, 1500), isNew: false);
|
||||
```
|
||||
|
||||
### Accessing Data
|
||||
```csharp
|
||||
// Access entire bar
|
||||
TBar lastBar = bars.Last;
|
||||
|
||||
// Access specific component series (Zero-Copy)
|
||||
TSeries closes = bars.Close;
|
||||
double lastClose = closes.Last.Value;
|
||||
|
||||
// Access via indexer
|
||||
TBar firstBar = bars[0];
|
||||
```
|
||||
|
||||
### Performance Note
|
||||
Because `TBarSeries` uses SoA layout, iterating over a single component (like `Close` prices) is extremely cache-efficient. The CPU prefetcher can load contiguous doubles without loading the interleaved Open, High, Low, or Volume data.
|
||||
# TBarSeries Class
|
||||
|
||||
`TBarSeries` is a high-performance collection of OHLCV bars implemented using a Structure of Arrays (SoA) layout. This design optimizes memory access patterns and enables efficient SIMD operations while providing convenient object-oriented views.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Structure of Arrays (SoA)**: Stores Time, Open, High, Low, Close, and Volume in separate contiguous arrays rather than an array of structs. This improves cache locality for operations that only need specific components (e.g., calculating SMA on Close prices).
|
||||
- **Zero-Copy Views**: Exposes `TSeries` properties (`Open`, `High`, `Low`, `Close`, `Volume`) that view the underlying data without copying.
|
||||
- **Streaming Support**: Efficiently handles real-time data updates with `Add(bar, isNew: false)`.
|
||||
- **Memory Efficient**: Minimizes object overhead by using shared internal lists.
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Views
|
||||
public TSeries Open { get; }
|
||||
public TSeries High { get; }
|
||||
public TSeries Low { get; }
|
||||
public TSeries Close { get; }
|
||||
public TSeries Volume { get; }
|
||||
|
||||
// Aliases
|
||||
public TSeries O => Open;
|
||||
public TSeries H => High;
|
||||
public TSeries L => Low;
|
||||
public TSeries C => Close;
|
||||
public TSeries V => Volume;
|
||||
}
|
||||
```
|
||||
|
||||
## Core Methods
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `Add(TBar bar, bool isNew = true)` | Adds a new bar or updates the last one. |
|
||||
| `Add(DateTime time, double o, double h, double l, double c, double v, bool isNew)` | Adds raw values directly. |
|
||||
| `Count` | Returns the number of bars. |
|
||||
| `Last` | Returns the most recent `TBar`. |
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Populating
|
||||
```csharp
|
||||
var bars = new TBarSeries();
|
||||
|
||||
// Add a new bar
|
||||
long now = DateTime.UtcNow.Ticks;
|
||||
bars.Add(new TBar(now, 100, 105, 95, 102, 1000), isNew: true);
|
||||
|
||||
// Update the last bar (e.g., real-time feed update)
|
||||
bars.Add(new TBar(now, 100, 106, 95, 104, 1500), isNew: false);
|
||||
```
|
||||
|
||||
### Accessing Data
|
||||
```csharp
|
||||
// Access entire bar
|
||||
TBar lastBar = bars.Last;
|
||||
|
||||
// Access specific component series (Zero-Copy)
|
||||
TSeries closes = bars.Close;
|
||||
double lastClose = closes.Last.Value;
|
||||
|
||||
// Access via indexer
|
||||
TBar firstBar = bars[0];
|
||||
```
|
||||
|
||||
### Performance Note
|
||||
Because `TBarSeries` uses SoA layout, iterating over a single component (like `Close` prices) is extremely cache-efficient. The CPU prefetcher can load contiguous doubles without loading the interleaved Open, High, Low, or Volume data.
|
||||
|
||||
+147
-147
@@ -1,147 +1,147 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
protected readonly List<long> _t = new();
|
||||
protected readonly List<double> _o = new();
|
||||
protected readonly List<double> _h = new();
|
||||
protected readonly List<double> _l = new();
|
||||
protected readonly List<double> _c = new();
|
||||
protected readonly List<double> _v = new();
|
||||
|
||||
public string Name { get; set; } = "Bar";
|
||||
public event Action<TBar>? Pub;
|
||||
|
||||
// Public properties are Views into the main data
|
||||
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()
|
||||
{
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
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" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
public TBarSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_o = new List<double>(capacity);
|
||||
_h = new List<double>(capacity);
|
||||
_l = new List<double>(capacity);
|
||||
_c = new List<double>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
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<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
|
||||
{
|
||||
_t.AddRange(t);
|
||||
_o.AddRange(o);
|
||||
_h.AddRange(h);
|
||||
_l.AddRange(l);
|
||||
_c.AddRange(c);
|
||||
_v.AddRange(v);
|
||||
}
|
||||
|
||||
public IEnumerator<TBar> 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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TBarSeries : IReadOnlyList<TBar>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
protected readonly List<long> _t = new();
|
||||
protected readonly List<double> _o = new();
|
||||
protected readonly List<double> _h = new();
|
||||
protected readonly List<double> _l = new();
|
||||
protected readonly List<double> _c = new();
|
||||
protected readonly List<double> _v = new();
|
||||
|
||||
public string Name { get; set; } = "Bar";
|
||||
public event Action<TBar>? Pub;
|
||||
|
||||
// Public properties are Views into the main data
|
||||
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()
|
||||
{
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
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" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
public TBarSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_o = new List<double>(capacity);
|
||||
_h = new List<double>(capacity);
|
||||
_l = new List<double>(capacity);
|
||||
_c = new List<double>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
|
||||
// Initialize views sharing the same Time list but different Value lists
|
||||
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<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
|
||||
{
|
||||
_t.AddRange(t);
|
||||
_o.AddRange(o);
|
||||
_h.AddRange(h);
|
||||
_l.AddRange(l);
|
||||
_c.AddRange(c);
|
||||
_v.AddRange(v);
|
||||
}
|
||||
|
||||
public IEnumerator<TBar> 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();
|
||||
}
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewValue_IncreasesCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(10.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateValue_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
series.Add(time, 11.0, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(11.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_MaintainsOrder()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long t0 = DateTime.UtcNow.Ticks;
|
||||
long t1 = t0 + TimeSpan.TicksPerMinute;
|
||||
|
||||
series.Add(t0, 10.0, isNew: true);
|
||||
series.Add(t1, 20.0, isNew: true);
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(10.0, series[0].Value);
|
||||
Assert.Equal(20.0, series[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TSeriesTests
|
||||
{
|
||||
[Fact]
|
||||
public void Add_NewValue_IncreasesCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(10.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_UpdateValue_DoesNotIncreaseCount()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
|
||||
series.Add(time, 10.0, isNew: true);
|
||||
series.Add(time, 11.0, isNew: false);
|
||||
|
||||
Assert.Single(series);
|
||||
Assert.Equal(11.0, series.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_MultipleValues_MaintainsOrder()
|
||||
{
|
||||
var series = new TSeries();
|
||||
long t0 = DateTime.UtcNow.Ticks;
|
||||
long t1 = t0 + TimeSpan.TicksPerMinute;
|
||||
|
||||
series.Add(t0, 10.0, isNew: true);
|
||||
series.Add(t1, 20.0, isNew: true);
|
||||
|
||||
Assert.Equal(2, series.Count);
|
||||
Assert.Equal(10.0, series[0].Value);
|
||||
Assert.Equal(20.0, series[1].Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+56
-56
@@ -1,56 +1,56 @@
|
||||
# TSeries: Time Series Data
|
||||
|
||||
## Overview
|
||||
|
||||
`TSeries` is a high-performance container for time-series data. Unlike a standard `List<TValue>`, it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays (`List<long>` and `List<double>`).
|
||||
|
||||
This layout is critical for performance because it allows:
|
||||
1. **SIMD Optimization**: The `Values` property returns a `ReadOnlySpan<double>` that can be directly processed by CPU vector instructions (AVX/SSE).
|
||||
2. **Cache Locality**: Iterating over values doesn't load timestamps into the CPU cache, and vice versa.
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal SoA storage
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
// Public accessors
|
||||
public ReadOnlySpan<double> Values => ...; // Zero-copy access
|
||||
public ReadOnlySpan<long> Times => ...; // Zero-copy access
|
||||
|
||||
public TValue Last { get; }
|
||||
public int Count { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **SoA Layout**: Optimized for numerical computing and SIMD.
|
||||
* **Zero-Copy Access**: `Values` and `Times` properties expose internal storage as Spans without copying.
|
||||
* **Streaming Support**: The `Add` method supports `isNew` parameter to handle intra-bar updates (replacing the last value instead of appending).
|
||||
* **Event Publishing**: Optional `Pub` event for reactive pipelines.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Adding Data
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.Now, 100.0); // isNew=true by default
|
||||
```
|
||||
|
||||
### Streaming Updates
|
||||
```csharp
|
||||
// New bar
|
||||
series.Add(time, 100.0, isNew: true);
|
||||
|
||||
// Update current bar (e.g. price change within same minute)
|
||||
series.Add(time, 101.0, isNew: false);
|
||||
```
|
||||
|
||||
### SIMD Processing
|
||||
```csharp
|
||||
// Calculate average using SIMD
|
||||
double avg = series.Values.AverageSIMD();
|
||||
# TSeries: Time Series Data
|
||||
|
||||
## Overview
|
||||
|
||||
`TSeries` is a high-performance container for time-series data. Unlike a standard `List<TValue>`, it uses a **Structure of Arrays (SoA)** layout internally. This means it stores timestamps and values in separate contiguous arrays (`List<long>` and `List<double>`).
|
||||
|
||||
This layout is critical for performance because it allows:
|
||||
1. **SIMD Optimization**: The `Values` property returns a `ReadOnlySpan<double>` that can be directly processed by CPU vector instructions (AVX/SSE).
|
||||
2. **Cache Locality**: Iterating over values doesn't load timestamps into the CPU cache, and vice versa.
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal SoA storage
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
// Public accessors
|
||||
public ReadOnlySpan<double> Values => ...; // Zero-copy access
|
||||
public ReadOnlySpan<long> Times => ...; // Zero-copy access
|
||||
|
||||
public TValue Last { get; }
|
||||
public int Count { get; }
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **SoA Layout**: Optimized for numerical computing and SIMD.
|
||||
* **Zero-Copy Access**: `Values` and `Times` properties expose internal storage as Spans without copying.
|
||||
* **Streaming Support**: The `Add` method supports `isNew` parameter to handle intra-bar updates (replacing the last value instead of appending).
|
||||
* **Event Publishing**: Optional `Pub` event for reactive pipelines.
|
||||
|
||||
## Usage
|
||||
|
||||
### Creating and Adding Data
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.Now, 100.0); // isNew=true by default
|
||||
```
|
||||
|
||||
### Streaming Updates
|
||||
```csharp
|
||||
// New bar
|
||||
series.Add(time, 100.0, isNew: true);
|
||||
|
||||
// Update current bar (e.g. price change within same minute)
|
||||
series.Add(time, 101.0, isNew: false);
|
||||
```
|
||||
|
||||
### SIMD Processing
|
||||
```csharp
|
||||
// Calculate average using SIMD
|
||||
double avg = series.Values.AverageSIMD();
|
||||
|
||||
+146
-146
@@ -1,146 +1,146 @@
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
// We use List<T> for dynamic sizing but access internal arrays via CollectionsMarshal for speed
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
public string Name { get; set; } = "Data";
|
||||
|
||||
// Event optimization: Use Action<TValue> to avoid EventArgs allocation
|
||||
// Note: Events are generally discouraged in the hot path of this high-perf design,
|
||||
// but kept for compatibility/chaining.
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
public TSeries()
|
||||
{
|
||||
_t = new List<long>();
|
||||
_v = new List<double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
public TSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for wrapping existing lists (e.g. from TBarSeries).
|
||||
/// </summary>
|
||||
public TSeries(List<long> time, List<double> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Value array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> Values
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> 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
|
||||
{
|
||||
// Update last bar
|
||||
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.Ticks, value), isNew);
|
||||
|
||||
public void Add(IEnumerable<double> values)
|
||||
{
|
||||
long t = DateTime.UtcNow.Ticks;
|
||||
foreach (var v in values)
|
||||
{
|
||||
Add(new TValue(t, v), isNew: true);
|
||||
t += TimeSpan.TicksPerMinute; // Dummy time increment
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation
|
||||
public IEnumerator<TValue> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _v.Count; i++)
|
||||
{
|
||||
yield return new TValue(_t[i], _v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
using System.Collections;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class TSeries : IReadOnlyList<TValue>
|
||||
{
|
||||
// Internal storage: SoA layout
|
||||
// We use List<T> for dynamic sizing but access internal arrays via CollectionsMarshal for speed
|
||||
protected readonly List<long> _t;
|
||||
protected readonly List<double> _v;
|
||||
|
||||
public string Name { get; set; } = "Data";
|
||||
|
||||
// Event optimization: Use Action<TValue> to avoid EventArgs allocation
|
||||
// Note: Events are generally discouraged in the hot path of this high-perf design,
|
||||
// but kept for compatibility/chaining.
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
public TSeries()
|
||||
{
|
||||
_t = new List<long>();
|
||||
_v = new List<double>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor with capacity hint to avoid List growth overhead.
|
||||
/// </summary>
|
||||
public TSeries(int capacity)
|
||||
{
|
||||
_t = new List<long>(capacity);
|
||||
_v = new List<double>(capacity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for wrapping existing lists (e.g. from TBarSeries).
|
||||
/// </summary>
|
||||
public TSeries(List<long> time, List<double> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Value array as a Span for SIMD operations.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<double> Values
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get => CollectionsMarshal.AsSpan(_v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Direct access to the underlying Time array as a Span.
|
||||
/// </summary>
|
||||
public ReadOnlySpan<long> 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
|
||||
{
|
||||
// Update last bar
|
||||
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.Ticks, value), isNew);
|
||||
|
||||
public void Add(IEnumerable<double> values)
|
||||
{
|
||||
long t = DateTime.UtcNow.Ticks;
|
||||
foreach (var v in values)
|
||||
{
|
||||
Add(new TValue(t, v), isNew: true);
|
||||
t += TimeSpan.TicksPerMinute; // Dummy time increment
|
||||
}
|
||||
}
|
||||
|
||||
// IEnumerable implementation
|
||||
public IEnumerator<TValue> GetEnumerator()
|
||||
{
|
||||
for (int i = 0; i < _v.Count; i++)
|
||||
{
|
||||
yield return new TValue(_t[i], _v[i]);
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TValueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
long ticks = dt.Ticks;
|
||||
var tValue = new TValue(ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains(dt.ToString("yyyy-MM-dd HH:mm:ss"), result);
|
||||
Assert.Contains("123.46", result); // Default formatting usually 2 decimals or similar
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
double val = tValue;
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests
|
||||
{
|
||||
public class TValueTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsPropertiesCorrectly()
|
||||
{
|
||||
long time = DateTime.UtcNow.Ticks;
|
||||
double value = 123.45;
|
||||
|
||||
var tValue = new TValue(time, value);
|
||||
|
||||
Assert.Equal(time, tValue.Time);
|
||||
Assert.Equal(value, tValue.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsDateTime_ReturnsCorrectDateTime()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
long ticks = dt.Ticks;
|
||||
var tValue = new TValue(ticks, 100.0);
|
||||
|
||||
Assert.Equal(dt, tValue.AsDateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToString_FormatsCorrectly()
|
||||
{
|
||||
DateTime dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var tValue = new TValue(dt.Ticks, 123.456);
|
||||
|
||||
string result = tValue.ToString();
|
||||
|
||||
Assert.Contains(dt.ToString("yyyy-MM-dd HH:mm:ss"), result);
|
||||
Assert.Contains("123.46", result); // Default formatting usually 2 decimals or similar
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImplicitConversion_ToDouble()
|
||||
{
|
||||
var tValue = new TValue(DateTime.UtcNow.Ticks, 42.0);
|
||||
double val = tValue;
|
||||
Assert.Equal(42.0, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-37
@@ -1,37 +1,37 @@
|
||||
# TValue: Time-Value Pair
|
||||
|
||||
## Overview
|
||||
|
||||
`TValue` is the fundamental building block of QuanTAlib. It represents a single data point in a time series, consisting of a timestamp and a double-precision floating-point value.
|
||||
|
||||
It is implemented as a lightweight `readonly struct` to ensure immutability and high performance (stack allocation, no GC overhead).
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public readonly struct TValue
|
||||
{
|
||||
public readonly long Time; // Ticks (UTC)
|
||||
public readonly double Value; // Data value
|
||||
public readonly bool IsNew; // Metadata for streaming (optional usage)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Lightweight**: 24 bytes (long + double + bool + padding).
|
||||
* **Immutable**: Thread-safe by design.
|
||||
* **Implicit Conversions**: Can be implicitly converted to `double` (returns Value) and `DateTime` (returns Time).
|
||||
* **Performance**: Designed for high-frequency trading and large dataset processing.
|
||||
|
||||
## Usage
|
||||
|
||||
`TValue` is used throughout the library for:
|
||||
* Input to indicators (`Update(TValue)`).
|
||||
* Output from indicators (`Value` property).
|
||||
* Elements in `TSeries`.
|
||||
|
||||
## Constructors
|
||||
|
||||
* `new TValue(long time, double value, bool isNew = true)`
|
||||
* `new TValue(DateTime time, double value, bool isNew = true)`
|
||||
# TValue: Time-Value Pair
|
||||
|
||||
## Overview
|
||||
|
||||
`TValue` is the fundamental building block of QuanTAlib. It represents a single data point in a time series, consisting of a timestamp and a double-precision floating-point value.
|
||||
|
||||
It is implemented as a lightweight `readonly struct` to ensure immutability and high performance (stack allocation, no GC overhead).
|
||||
|
||||
## Structure
|
||||
|
||||
```csharp
|
||||
public readonly struct TValue
|
||||
{
|
||||
public readonly long Time; // Ticks (UTC)
|
||||
public readonly double Value; // Data value
|
||||
public readonly bool IsNew; // Metadata for streaming (optional usage)
|
||||
}
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
* **Lightweight**: 24 bytes (long + double + bool + padding).
|
||||
* **Immutable**: Thread-safe by design.
|
||||
* **Implicit Conversions**: Can be implicitly converted to `double` (returns Value) and `DateTime` (returns Time).
|
||||
* **Performance**: Designed for high-frequency trading and large dataset processing.
|
||||
|
||||
## Usage
|
||||
|
||||
`TValue` is used throughout the library for:
|
||||
* Input to indicators (`Update(TValue)`).
|
||||
* Output from indicators (`Value` property).
|
||||
* Elements in `TSeries`.
|
||||
|
||||
## Constructors
|
||||
|
||||
* `new TValue(long time, double value, bool isNew = true)`
|
||||
* `new TValue(DateTime time, double value, bool isNew = true)`
|
||||
|
||||
+57
-57
@@ -1,57 +1,57 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing a time-value pair.
|
||||
/// Pure data type: 16 bytes (long + double).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TValue : IEquatable<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Time in ticks (UTC).
|
||||
/// </summary>
|
||||
public readonly long Time;
|
||||
|
||||
/// <summary>
|
||||
/// The value.
|
||||
/// </summary>
|
||||
public readonly double Value;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property to get DateTime from Ticks.
|
||||
/// </summary>
|
||||
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.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;
|
||||
|
||||
/// <summary>
|
||||
/// A lightweight struct representing a time-value pair.
|
||||
/// Pure data type: 16 bytes (long + double).
|
||||
/// </summary>
|
||||
[SkipLocalsInit]
|
||||
public readonly struct TValue : IEquatable<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Time in ticks (UTC).
|
||||
/// </summary>
|
||||
public readonly long Time;
|
||||
|
||||
/// <summary>
|
||||
/// The value.
|
||||
/// </summary>
|
||||
public readonly double Value;
|
||||
|
||||
/// <summary>
|
||||
/// Convenience property to get DateTime from Ticks.
|
||||
/// </summary>
|
||||
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.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);
|
||||
}
|
||||
|
||||
+35
-35
@@ -1,35 +1,35 @@
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for data feeds that provide TBar (OHLCV) data.
|
||||
/// Implementations include synthetic generators (GBM), API-based feeds (AlphaVantage),
|
||||
/// file readers (CSV), and real-time streams (WebSocket).
|
||||
/// </summary>
|
||||
public interface IFeed
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the next bar from the feed with full bidirectional control.
|
||||
/// </summary>
|
||||
/// <param name="isNew">
|
||||
/// Input: Request for new bar (true) or update current bar (false).
|
||||
/// Output: Actual behavior - may differ if feed cannot honor request (e.g., end of data).
|
||||
/// </param>
|
||||
/// <returns>The bar (new or updated)</returns>
|
||||
TBar Next(ref bool isNew);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar from the feed with simple control.
|
||||
/// </summary>
|
||||
/// <param name="isNew">Request for new bar (true) or update current bar (false). Defaults to true.</param>
|
||||
/// <returns>The bar (new or updated)</returns>
|
||||
TBar Next(bool isNew = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple bars in batch with explicit time parameters.
|
||||
/// </summary>
|
||||
/// <param name="count">Number of bars to retrieve</param>
|
||||
/// <param name="startTime">Starting timestamp for first bar (in ticks)</param>
|
||||
/// <param name="interval">Time interval between bars</param>
|
||||
/// <returns>Series containing the requested bars</returns>
|
||||
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for data feeds that provide TBar (OHLCV) data.
|
||||
/// Implementations include synthetic generators (GBM), API-based feeds (AlphaVantage),
|
||||
/// file readers (CSV), and real-time streams (WebSocket).
|
||||
/// </summary>
|
||||
public interface IFeed
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the next bar from the feed with full bidirectional control.
|
||||
/// </summary>
|
||||
/// <param name="isNew">
|
||||
/// Input: Request for new bar (true) or update current bar (false).
|
||||
/// Output: Actual behavior - may differ if feed cannot honor request (e.g., end of data).
|
||||
/// </param>
|
||||
/// <returns>The bar (new or updated)</returns>
|
||||
TBar Next(ref bool isNew);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar from the feed with simple control.
|
||||
/// </summary>
|
||||
/// <param name="isNew">Request for new bar (true) or update current bar (false). Defaults to true.</param>
|
||||
/// <returns>The bar (new or updated)</returns>
|
||||
TBar Next(bool isNew = true);
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple bars in batch with explicit time parameters.
|
||||
/// </summary>
|
||||
/// <param name="count">Number of bars to retrieve</param>
|
||||
/// <param name="startTime">Starting timestamp for first bar (in ticks)</param>
|
||||
/// <param name="interval">Time interval between bars</param>
|
||||
/// <returns>Series containing the requested bars</returns>
|
||||
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
|
||||
+45
-45
@@ -1,45 +1,45 @@
|
||||
# IFeed Interface
|
||||
|
||||
`IFeed` defines the standard contract for all data feeds in QuanTAlib, ensuring consistent behavior across different data sources (synthetic, file-based, or live API).
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Bidirectional Control**: The `Next(ref bool isNew)` method allows the consumer to request a new bar (`isNew = true`) or an update to the current bar (`isNew = false`).
|
||||
- **Streaming**: Designed for bar-by-bar processing, simulating real-time data flow.
|
||||
- **Batching**: Supports fetching historical data ranges via `Fetch()`.
|
||||
|
||||
## Interface Definition
|
||||
|
||||
```csharp
|
||||
public interface IFeed
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the next bar with full control over new/update state.
|
||||
/// </summary>
|
||||
TBar Next(ref bool isNew);
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload for simple next-bar requests.
|
||||
/// </summary>
|
||||
TBar Next(bool isNew = true);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a batch of historical bars.
|
||||
/// </summary>
|
||||
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
When implementing `IFeed`:
|
||||
|
||||
1. **State Management**: Maintain the current position in the data source.
|
||||
2. **End of Data**: When data is exhausted, `Next` should return the last valid bar and set `isNew` to `false`.
|
||||
3. **Intra-bar Updates**: If the source supports it (e.g., live ticks), `Next(isNew: false)` should return the updated state of the current bar. If not supported (e.g., CSV), it should return the current bar unchanged.
|
||||
4. **Thread Safety**: Implementations are generally not required to be thread-safe unless specified.
|
||||
|
||||
## Implementations
|
||||
|
||||
- **`GBM`**: Geometric Brownian Motion generator (Synthetic).
|
||||
- **`CsvFeed`**: Reads OHLCV data from CSV files (Historical).
|
||||
# IFeed Interface
|
||||
|
||||
`IFeed` defines the standard contract for all data feeds in QuanTAlib, ensuring consistent behavior across different data sources (synthetic, file-based, or live API).
|
||||
|
||||
## Key Concepts
|
||||
|
||||
- **Bidirectional Control**: The `Next(ref bool isNew)` method allows the consumer to request a new bar (`isNew = true`) or an update to the current bar (`isNew = false`).
|
||||
- **Streaming**: Designed for bar-by-bar processing, simulating real-time data flow.
|
||||
- **Batching**: Supports fetching historical data ranges via `Fetch()`.
|
||||
|
||||
## Interface Definition
|
||||
|
||||
```csharp
|
||||
public interface IFeed
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the next bar with full control over new/update state.
|
||||
/// </summary>
|
||||
TBar Next(ref bool isNew);
|
||||
|
||||
/// <summary>
|
||||
/// Convenience overload for simple next-bar requests.
|
||||
/// </summary>
|
||||
TBar Next(bool isNew = true);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a batch of historical bars.
|
||||
/// </summary>
|
||||
TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
When implementing `IFeed`:
|
||||
|
||||
1. **State Management**: Maintain the current position in the data source.
|
||||
2. **End of Data**: When data is exhausted, `Next` should return the last valid bar and set `isNew` to `false`.
|
||||
3. **Intra-bar Updates**: If the source supports it (e.g., live ticks), `Next(isNew: false)` should return the updated state of the current bar. If not supported (e.g., CSV), it should return the current bar unchanged.
|
||||
4. **Thread Safety**: Implementations are generally not required to be thread-safe unless specified.
|
||||
|
||||
## Implementations
|
||||
|
||||
- **`GBM`**: Geometric Brownian Motion generator (Synthetic).
|
||||
- **`CsvFeed`**: Reads OHLCV data from CSV files (Historical).
|
||||
|
||||
+254
-254
@@ -1,254 +1,254 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CsvFeedTests
|
||||
{
|
||||
private const string TestCsvPath = "daily_IBM.csv";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidFile_LoadsData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
Assert.NotNull(feed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
Assert.Throws<FileNotFoundException>(() => new CsvFeed("nonexistent.csv"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CsvFeed(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EmptyPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CsvFeed(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_StreamsDataChronologically()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
// Get second bar - should be later in time
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
|
||||
// Get third bar
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_WithRefParameter_StreamsCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
bool isNew = true;
|
||||
var bar1 = feed.Next(ref isNew);
|
||||
Assert.True(isNew); // Should still be true
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
isNew = true;
|
||||
var bar2 = feed.Next(ref isNew);
|
||||
Assert.True(isNew);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_UpdateCurrentBar_ReturnsSameBar()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
|
||||
// Update current bar (should return same bar)
|
||||
var bar2 = feed.Next(isNew: false);
|
||||
Assert.Equal(bar1.Time, bar2.Time);
|
||||
Assert.Equal(bar1.Close, bar2.Close);
|
||||
|
||||
// Get next bar
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_EndOfData_SignalsNoMoreData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Stream through all data
|
||||
TBar lastBar = default;
|
||||
bool isNew = true;
|
||||
int count = 0;
|
||||
|
||||
while (isNew && count < 200) // Safety limit
|
||||
{
|
||||
lastBar = feed.Next(ref isNew);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Should have reached end and isNew should be false
|
||||
Assert.False(isNew);
|
||||
Assert.True(lastBar.Time > 0);
|
||||
|
||||
// Calling again should return same bar with isNew=false
|
||||
isNew = true;
|
||||
var finalBar = feed.Next(ref isNew);
|
||||
Assert.False(isNew);
|
||||
Assert.Equal(lastBar.Time, finalBar.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_ReturnsCorrectNumberOfBars()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
var series = feed.Fetch(10, startTime, interval);
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
Assert.True(series.Count <= 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_InvalidCount_ThrowsArgumentException()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => feed.Fetch(0, startTime, interval));
|
||||
Assert.Throws<ArgumentException>(() => feed.Fetch(-1, startTime, interval));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_ResetsStreamingPosition()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Stream a few bars
|
||||
feed.Next(isNew: true);
|
||||
feed.Next(isNew: true);
|
||||
feed.Next(isNew: true);
|
||||
|
||||
// Fetch from start
|
||||
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Next should now stream from fetched position
|
||||
var bar = feed.Next(isNew: true);
|
||||
Assert.True(bar.Time >= startTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromCsv_ParsesValuesCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar (oldest in chronological order)
|
||||
var bar = feed.Next(isNew: true);
|
||||
|
||||
// Verify it has valid OHLCV data
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High >= bar.Open);
|
||||
Assert.True(bar.High >= bar.Close);
|
||||
Assert.True(bar.Low <= bar.Open);
|
||||
Assert.True(bar.Low <= bar.Close);
|
||||
Assert.True(bar.Close > 0);
|
||||
Assert.True(bar.Volume > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromCsv_DataInChronologicalOrder()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bars = new List<TBar>();
|
||||
bool isNew = true;
|
||||
|
||||
// Collect first 10 bars
|
||||
for (int i = 0; i < 10 && isNew; i++)
|
||||
{
|
||||
bars.Add(feed.Next(ref isNew));
|
||||
}
|
||||
|
||||
// Verify chronological order (each bar later than previous)
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
Assert.True(bars[i].Time > bars[i - 1].Time,
|
||||
$"Bar {i} time ({bars[i].AsDateTime}) should be after bar {i-1} time ({bars[i-1].AsDateTime})");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CsvFeed_WorksWithIFeedInterface()
|
||||
{
|
||||
IFeed feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_MixedNewAndUpdate_WorksCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
var bar1Update = feed.Next(isNew: false);
|
||||
Assert.Equal(bar1.Time, bar1Update.Time);
|
||||
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
|
||||
var bar2Update = feed.Next(isNew: false);
|
||||
Assert.Equal(bar2.Time, bar2Update.Time);
|
||||
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithEarlyStartTime_ReturnsData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Start from very early date (before any data)
|
||||
var startTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Should return data starting from first available bar
|
||||
Assert.True(series.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithFutureStartTime_ReturnsEmpty()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Start from future date (after all data)
|
||||
var startTime = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Should return empty or minimal data
|
||||
Assert.True(series.Count == 0);
|
||||
}
|
||||
}
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CsvFeedTests
|
||||
{
|
||||
private const string TestCsvPath = "daily_IBM.csv";
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidFile_LoadsData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
Assert.NotNull(feed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NonExistentFile_ThrowsFileNotFoundException()
|
||||
{
|
||||
Assert.Throws<FileNotFoundException>(() => new CsvFeed("nonexistent.csv"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NullPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CsvFeed(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_EmptyPath_ThrowsArgumentException()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new CsvFeed(""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_StreamsDataChronologically()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
// Get second bar - should be later in time
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
|
||||
// Get third bar
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_WithRefParameter_StreamsCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
bool isNew = true;
|
||||
var bar1 = feed.Next(ref isNew);
|
||||
Assert.True(isNew); // Should still be true
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
isNew = true;
|
||||
var bar2 = feed.Next(ref isNew);
|
||||
Assert.True(isNew);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_UpdateCurrentBar_ReturnsSameBar()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
|
||||
// Update current bar (should return same bar)
|
||||
var bar2 = feed.Next(isNew: false);
|
||||
Assert.Equal(bar1.Time, bar2.Time);
|
||||
Assert.Equal(bar1.Close, bar2.Close);
|
||||
|
||||
// Get next bar
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_EndOfData_SignalsNoMoreData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Stream through all data
|
||||
TBar lastBar = default;
|
||||
bool isNew = true;
|
||||
int count = 0;
|
||||
|
||||
while (isNew && count < 200) // Safety limit
|
||||
{
|
||||
lastBar = feed.Next(ref isNew);
|
||||
count++;
|
||||
}
|
||||
|
||||
// Should have reached end and isNew should be false
|
||||
Assert.False(isNew);
|
||||
Assert.True(lastBar.Time > 0);
|
||||
|
||||
// Calling again should return same bar with isNew=false
|
||||
isNew = true;
|
||||
var finalBar = feed.Next(ref isNew);
|
||||
Assert.False(isNew);
|
||||
Assert.Equal(lastBar.Time, finalBar.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_ReturnsCorrectNumberOfBars()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
var series = feed.Fetch(10, startTime, interval);
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
Assert.True(series.Count <= 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_InvalidCount_ThrowsArgumentException()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromDays(1);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => feed.Fetch(0, startTime, interval));
|
||||
Assert.Throws<ArgumentException>(() => feed.Fetch(-1, startTime, interval));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_ResetsStreamingPosition()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Stream a few bars
|
||||
feed.Next(isNew: true);
|
||||
feed.Next(isNew: true);
|
||||
feed.Next(isNew: true);
|
||||
|
||||
// Fetch from start
|
||||
var startTime = new DateTime(2025, 7, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Next should now stream from fetched position
|
||||
var bar = feed.Next(isNew: true);
|
||||
Assert.True(bar.Time >= startTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromCsv_ParsesValuesCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Get first bar (oldest in chronological order)
|
||||
var bar = feed.Next(isNew: true);
|
||||
|
||||
// Verify it has valid OHLCV data
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High >= bar.Open);
|
||||
Assert.True(bar.High >= bar.Close);
|
||||
Assert.True(bar.Low <= bar.Open);
|
||||
Assert.True(bar.Low <= bar.Close);
|
||||
Assert.True(bar.Close > 0);
|
||||
Assert.True(bar.Volume > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadFromCsv_DataInChronologicalOrder()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bars = new List<TBar>();
|
||||
bool isNew = true;
|
||||
|
||||
// Collect first 10 bars
|
||||
for (int i = 0; i < 10 && isNew; i++)
|
||||
{
|
||||
bars.Add(feed.Next(ref isNew));
|
||||
}
|
||||
|
||||
// Verify chronological order (each bar later than previous)
|
||||
for (int i = 1; i < bars.Count; i++)
|
||||
{
|
||||
Assert.True(bars[i].Time > bars[i - 1].Time,
|
||||
$"Bar {i} time ({bars[i].AsDateTime}) should be after bar {i-1} time ({bars[i-1].AsDateTime})");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CsvFeed_WorksWithIFeedInterface()
|
||||
{
|
||||
IFeed feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
Assert.True(bar1.Time > 0);
|
||||
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_MixedNewAndUpdate_WorksCorrectly()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
var bar1 = feed.Next(isNew: true);
|
||||
var bar1Update = feed.Next(isNew: false);
|
||||
Assert.Equal(bar1.Time, bar1Update.Time);
|
||||
|
||||
var bar2 = feed.Next(isNew: true);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
|
||||
var bar2Update = feed.Next(isNew: false);
|
||||
Assert.Equal(bar2.Time, bar2Update.Time);
|
||||
|
||||
var bar3 = feed.Next(isNew: true);
|
||||
Assert.True(bar3.Time > bar2.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithEarlyStartTime_ReturnsData()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Start from very early date (before any data)
|
||||
var startTime = new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Should return data starting from first available bar
|
||||
Assert.True(series.Count > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithFutureStartTime_ReturnsEmpty()
|
||||
{
|
||||
var feed = new CsvFeed(TestCsvPath);
|
||||
|
||||
// Start from future date (after all data)
|
||||
var startTime = new DateTime(2030, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks;
|
||||
var series = feed.Fetch(5, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
// Should return empty or minimal data
|
||||
Assert.True(series.Count == 0);
|
||||
}
|
||||
}
|
||||
|
||||
+196
-196
@@ -1,196 +1,196 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// CSV file feed for loading historical OHLCV data.
|
||||
/// Loads data in constructor and streams through it with Next() or returns batches with Fetch().
|
||||
/// CSV format: timestamp,open,high,low,close,volume (header required)
|
||||
/// Timestamp format: YYYY-MM-DD (UTC midnight assumed)
|
||||
/// </summary>
|
||||
public class CsvFeed : IFeed
|
||||
{
|
||||
private readonly TBarSeries _data;
|
||||
|
||||
// Streaming state
|
||||
private int _currentIndex;
|
||||
private TBar _currentBar;
|
||||
private bool _hasCurrentBar;
|
||||
|
||||
/// <summary>
|
||||
/// Loads CSV file and prepares data for streaming.
|
||||
/// Data is reversed to chronological order (oldest first).
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to CSV file</param>
|
||||
public CsvFeed(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"CSV file not found: {filePath}", filePath);
|
||||
|
||||
_data = LoadFromCsv(filePath);
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses CSV file into TBarSeries.
|
||||
/// Expected format: timestamp,open,high,low,close,volume
|
||||
/// </summary>
|
||||
private static TBarSeries LoadFromCsv(string filePath)
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
|
||||
if (lines.Length == 0)
|
||||
throw new InvalidDataException("CSV file is empty");
|
||||
|
||||
// Skip header, reverse to chronological order (oldest first)
|
||||
var dataLines = lines.Skip(1).Reverse().ToArray();
|
||||
|
||||
if (dataLines.Length == 0)
|
||||
throw new InvalidDataException("CSV file contains only header, no data");
|
||||
|
||||
var series = new TBarSeries(dataLines.Length);
|
||||
|
||||
for (int i = 0; i < dataLines.Length; i++)
|
||||
{
|
||||
var line = dataLines[i];
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(',');
|
||||
if (parts.Length != 6)
|
||||
throw new FormatException($"Invalid CSV format at line {i + 2}. Expected 6 columns, found {parts.Length}");
|
||||
|
||||
try
|
||||
{
|
||||
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
|
||||
var timestamp = DateTime.ParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
||||
|
||||
// Parse OHLCV values
|
||||
double open = double.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
|
||||
double high = double.Parse(parts[2].Trim(), CultureInfo.InvariantCulture);
|
||||
double low = double.Parse(parts[3].Trim(), CultureInfo.InvariantCulture);
|
||||
double close = double.Parse(parts[4].Trim(), CultureInfo.InvariantCulture);
|
||||
double volume = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture);
|
||||
|
||||
series.Add(timestamp, open, high, low, close, volume, isNew: true);
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException or OverflowException)
|
||||
{
|
||||
throw new FormatException($"Failed to parse CSV line {i + 2}: {line}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with full bidirectional control.
|
||||
/// When end of data reached, returns last bar and sets isNew=false.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(ref bool isNew)
|
||||
{
|
||||
if (_data.Count == 0)
|
||||
{
|
||||
isNew = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (isNew || !_hasCurrentBar)
|
||||
{
|
||||
// Request for new bar
|
||||
if (_currentIndex >= _data.Count)
|
||||
{
|
||||
// End of data - return last bar and signal no more data
|
||||
isNew = false;
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
_currentBar = _data[_currentIndex];
|
||||
_currentIndex++;
|
||||
_hasCurrentBar = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update current bar - CSV has no intra-bar updates, return same bar
|
||||
// No change to _currentBar or _currentIndex
|
||||
}
|
||||
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with simple control.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(bool isNew = true)
|
||||
{
|
||||
return Next(ref isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a filtered subset of data matching the criteria.
|
||||
/// Resets streaming position to start of returned data.
|
||||
/// </summary>
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be positive", nameof(count));
|
||||
|
||||
var result = new TBarSeries(count);
|
||||
|
||||
// Find starting index
|
||||
int startIndex = 0;
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
if (_data[i].Time >= startTime)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect bars matching interval
|
||||
long expectedTime = startTime;
|
||||
int collected = 0;
|
||||
|
||||
for (int i = startIndex; i < _data.Count && collected < count; i++)
|
||||
{
|
||||
var bar = _data[i];
|
||||
|
||||
// Check if bar time matches expected time (within tolerance)
|
||||
long timeDiff = Math.Abs(bar.Time - expectedTime);
|
||||
long tolerance = interval.Ticks / 2; // Allow 50% tolerance
|
||||
|
||||
if (timeDiff <= tolerance)
|
||||
{
|
||||
result.Add(bar, isNew: true);
|
||||
collected++;
|
||||
expectedTime += interval.Ticks;
|
||||
}
|
||||
else if (bar.Time > expectedTime)
|
||||
{
|
||||
// Gap in data - skip forward
|
||||
long gaps = (bar.Time - expectedTime) / interval.Ticks;
|
||||
expectedTime += (gaps + 1) * interval.Ticks;
|
||||
|
||||
if (Math.Abs(bar.Time - expectedTime + interval.Ticks) <= tolerance)
|
||||
{
|
||||
result.Add(bar, isNew: true);
|
||||
collected++;
|
||||
expectedTime += interval.Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset streaming to start of returned data
|
||||
_currentIndex = startIndex;
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// CSV file feed for loading historical OHLCV data.
|
||||
/// Loads data in constructor and streams through it with Next() or returns batches with Fetch().
|
||||
/// CSV format: timestamp,open,high,low,close,volume (header required)
|
||||
/// Timestamp format: YYYY-MM-DD (UTC midnight assumed)
|
||||
/// </summary>
|
||||
public class CsvFeed : IFeed
|
||||
{
|
||||
private readonly TBarSeries _data;
|
||||
|
||||
// Streaming state
|
||||
private int _currentIndex;
|
||||
private TBar _currentBar;
|
||||
private bool _hasCurrentBar;
|
||||
|
||||
/// <summary>
|
||||
/// Loads CSV file and prepares data for streaming.
|
||||
/// Data is reversed to chronological order (oldest first).
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to CSV file</param>
|
||||
public CsvFeed(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
throw new FileNotFoundException($"CSV file not found: {filePath}", filePath);
|
||||
|
||||
_data = LoadFromCsv(filePath);
|
||||
_currentIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses CSV file into TBarSeries.
|
||||
/// Expected format: timestamp,open,high,low,close,volume
|
||||
/// </summary>
|
||||
private static TBarSeries LoadFromCsv(string filePath)
|
||||
{
|
||||
var lines = File.ReadAllLines(filePath);
|
||||
|
||||
if (lines.Length == 0)
|
||||
throw new InvalidDataException("CSV file is empty");
|
||||
|
||||
// Skip header, reverse to chronological order (oldest first)
|
||||
var dataLines = lines.Skip(1).Reverse().ToArray();
|
||||
|
||||
if (dataLines.Length == 0)
|
||||
throw new InvalidDataException("CSV file contains only header, no data");
|
||||
|
||||
var series = new TBarSeries(dataLines.Length);
|
||||
|
||||
for (int i = 0; i < dataLines.Length; i++)
|
||||
{
|
||||
var line = dataLines[i];
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
continue;
|
||||
|
||||
var parts = line.Split(',');
|
||||
if (parts.Length != 6)
|
||||
throw new FormatException($"Invalid CSV format at line {i + 2}. Expected 6 columns, found {parts.Length}");
|
||||
|
||||
try
|
||||
{
|
||||
// Parse timestamp (YYYY-MM-DD format, assume UTC midnight)
|
||||
var timestamp = DateTime.ParseExact(parts[0].Trim(), "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
|
||||
|
||||
// Parse OHLCV values
|
||||
double open = double.Parse(parts[1].Trim(), CultureInfo.InvariantCulture);
|
||||
double high = double.Parse(parts[2].Trim(), CultureInfo.InvariantCulture);
|
||||
double low = double.Parse(parts[3].Trim(), CultureInfo.InvariantCulture);
|
||||
double close = double.Parse(parts[4].Trim(), CultureInfo.InvariantCulture);
|
||||
double volume = double.Parse(parts[5].Trim(), CultureInfo.InvariantCulture);
|
||||
|
||||
series.Add(timestamp, open, high, low, close, volume, isNew: true);
|
||||
}
|
||||
catch (Exception ex) when (ex is FormatException or OverflowException)
|
||||
{
|
||||
throw new FormatException($"Failed to parse CSV line {i + 2}: {line}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with full bidirectional control.
|
||||
/// When end of data reached, returns last bar and sets isNew=false.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(ref bool isNew)
|
||||
{
|
||||
if (_data.Count == 0)
|
||||
{
|
||||
isNew = false;
|
||||
return default;
|
||||
}
|
||||
|
||||
if (isNew || !_hasCurrentBar)
|
||||
{
|
||||
// Request for new bar
|
||||
if (_currentIndex >= _data.Count)
|
||||
{
|
||||
// End of data - return last bar and signal no more data
|
||||
isNew = false;
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
_currentBar = _data[_currentIndex];
|
||||
_currentIndex++;
|
||||
_hasCurrentBar = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update current bar - CSV has no intra-bar updates, return same bar
|
||||
// No change to _currentBar or _currentIndex
|
||||
}
|
||||
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with simple control.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(bool isNew = true)
|
||||
{
|
||||
return Next(ref isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a filtered subset of data matching the criteria.
|
||||
/// Resets streaming position to start of returned data.
|
||||
/// </summary>
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be positive", nameof(count));
|
||||
|
||||
var result = new TBarSeries(count);
|
||||
|
||||
// Find starting index
|
||||
int startIndex = 0;
|
||||
for (int i = 0; i < _data.Count; i++)
|
||||
{
|
||||
if (_data[i].Time >= startTime)
|
||||
{
|
||||
startIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect bars matching interval
|
||||
long expectedTime = startTime;
|
||||
int collected = 0;
|
||||
|
||||
for (int i = startIndex; i < _data.Count && collected < count; i++)
|
||||
{
|
||||
var bar = _data[i];
|
||||
|
||||
// Check if bar time matches expected time (within tolerance)
|
||||
long timeDiff = Math.Abs(bar.Time - expectedTime);
|
||||
long tolerance = interval.Ticks / 2; // Allow 50% tolerance
|
||||
|
||||
if (timeDiff <= tolerance)
|
||||
{
|
||||
result.Add(bar, isNew: true);
|
||||
collected++;
|
||||
expectedTime += interval.Ticks;
|
||||
}
|
||||
else if (bar.Time > expectedTime)
|
||||
{
|
||||
// Gap in data - skip forward
|
||||
long gaps = (bar.Time - expectedTime) / interval.Ticks;
|
||||
expectedTime += (gaps + 1) * interval.Ticks;
|
||||
|
||||
if (Math.Abs(bar.Time - expectedTime + interval.Ticks) <= tolerance)
|
||||
{
|
||||
result.Add(bar, isNew: true);
|
||||
collected++;
|
||||
expectedTime += interval.Ticks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset streaming to start of returned data
|
||||
_currentIndex = startIndex;
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+68
-68
@@ -1,68 +1,68 @@
|
||||
# CsvFeed Class
|
||||
|
||||
`CsvFeed` is a file-based feed implementation that loads historical OHLCV data from CSV files. It supports both streaming access (simulating real-time playback) and batch retrieval.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Historical Data Loading**: Reads standard OHLCV CSV files.
|
||||
- **Chronological Ordering**: Automatically reverses data if needed (assumes newest-first in file, provides oldest-first).
|
||||
- **Streaming Interface**: Implements `IFeed` for consistent usage with other feed types.
|
||||
- **Batch Retrieval**: Supports fetching specific time ranges via `Fetch()`.
|
||||
|
||||
## CSV Format Requirements
|
||||
|
||||
The file must have a header row and follow this column order:
|
||||
`timestamp, open, high, low, close, volume`
|
||||
|
||||
- **Timestamp**: `YYYY-MM-DD` (assumed UTC midnight)
|
||||
- **Prices/Volume**: Numeric values
|
||||
|
||||
Example:
|
||||
```csv
|
||||
Date,Open,High,Low,Close,Volume
|
||||
2024-01-01,100.0,105.0,99.0,102.5,10000
|
||||
2024-01-02,102.5,103.0,101.0,101.5,8500
|
||||
```
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class CsvFeed : IFeed
|
||||
{
|
||||
public CsvFeed(string filePath);
|
||||
public TBar Next(bool isNew = true);
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Loading Data
|
||||
```csharp
|
||||
var feed = new CsvFeed("path/to/data.csv");
|
||||
```
|
||||
|
||||
### 2. Streaming Data (Simulation)
|
||||
```csharp
|
||||
// Get first bar
|
||||
var bar = feed.Next(isNew: true);
|
||||
|
||||
// Loop through all data
|
||||
while (true)
|
||||
{
|
||||
// Process bar...
|
||||
Console.WriteLine(bar);
|
||||
|
||||
// Get next bar
|
||||
bool isNew = true;
|
||||
bar = feed.Next(ref isNew);
|
||||
|
||||
// Stop if no more new data
|
||||
if (!isNew) break;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Fetching a Batch
|
||||
```csharp
|
||||
long startTime = new DateTime(2024, 1, 1).Ticks;
|
||||
var batch = feed.Fetch(10, startTime, TimeSpan.FromDays(1));
|
||||
# CsvFeed Class
|
||||
|
||||
`CsvFeed` is a file-based feed implementation that loads historical OHLCV data from CSV files. It supports both streaming access (simulating real-time playback) and batch retrieval.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Historical Data Loading**: Reads standard OHLCV CSV files.
|
||||
- **Chronological Ordering**: Automatically reverses data if needed (assumes newest-first in file, provides oldest-first).
|
||||
- **Streaming Interface**: Implements `IFeed` for consistent usage with other feed types.
|
||||
- **Batch Retrieval**: Supports fetching specific time ranges via `Fetch()`.
|
||||
|
||||
## CSV Format Requirements
|
||||
|
||||
The file must have a header row and follow this column order:
|
||||
`timestamp, open, high, low, close, volume`
|
||||
|
||||
- **Timestamp**: `YYYY-MM-DD` (assumed UTC midnight)
|
||||
- **Prices/Volume**: Numeric values
|
||||
|
||||
Example:
|
||||
```csv
|
||||
Date,Open,High,Low,Close,Volume
|
||||
2024-01-01,100.0,105.0,99.0,102.5,10000
|
||||
2024-01-02,102.5,103.0,101.0,101.5,8500
|
||||
```
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class CsvFeed : IFeed
|
||||
{
|
||||
public CsvFeed(string filePath);
|
||||
public TBar Next(bool isNew = true);
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Loading Data
|
||||
```csharp
|
||||
var feed = new CsvFeed("path/to/data.csv");
|
||||
```
|
||||
|
||||
### 2. Streaming Data (Simulation)
|
||||
```csharp
|
||||
// Get first bar
|
||||
var bar = feed.Next(isNew: true);
|
||||
|
||||
// Loop through all data
|
||||
while (true)
|
||||
{
|
||||
// Process bar...
|
||||
Console.WriteLine(bar);
|
||||
|
||||
// Get next bar
|
||||
bool isNew = true;
|
||||
bar = feed.Next(ref isNew);
|
||||
|
||||
// Stop if no more new data
|
||||
if (!isNew) break;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Fetching a Batch
|
||||
```csharp
|
||||
long startTime = new DateTime(2024, 1, 1).Ticks;
|
||||
var batch = feed.Fetch(10, startTime, TimeSpan.FromDays(1));
|
||||
|
||||
+101
-101
@@ -1,101 +1,101 @@
|
||||
timestamp,open,high,low,close,volume
|
||||
2025-11-25,304.1250,306.0000,297.0600,304.4800,2825322
|
||||
2025-11-24,299.1800,307.1800,297.5100,304.1200,6050640
|
||||
2025-11-21,293.4800,300.4800,291.8900,297.4400,5710903
|
||||
2025-11-20,294.6400,300.7100,290.1600,290.4000,5597028
|
||||
2025-11-19,290.5000,291.1099,288.0700,288.5300,3595912
|
||||
2025-11-18,297.0000,297.0000,289.9200,289.9500,4861928
|
||||
2025-11-17,305.5900,306.0000,296.5100,297.1700,3909741
|
||||
2025-11-14,300.0000,307.7200,297.5900,305.6900,3592455
|
||||
2025-11-13,312.2900,314.6000,303.6800,304.8600,5310150
|
||||
2025-11-12,319.8900,324.9000,314.5324,314.9800,6042686
|
||||
2025-11-11,309.0000,317.9100,308.4300,313.7200,4381913
|
||||
2025-11-10,306.8200,309.9400,304.2300,309.1300,2975188
|
||||
2025-11-07,309.6800,310.0000,302.6301,306.3800,5070773
|
||||
2025-11-06,306.7500,315.4400,301.0900,312.4200,6818521
|
||||
2025-11-05,301.3800,307.2000,299.7100,306.7700,4633195
|
||||
2025-11-04,300.0000,303.1700,296.0000,300.8500,5677330
|
||||
2025-11-03,308.0000,312.1411,304.2300,304.7300,4957958
|
||||
2025-10-31,312.0000,313.5000,301.6300,307.4100,7697499
|
||||
2025-10-30,306.6500,313.7500,305.0200,310.0600,4694275
|
||||
2025-10-29,312.7900,314.3300,307.5200,308.2100,4135948
|
||||
2025-10-28,312.6000,319.3500,311.4100,312.5700,6044770
|
||||
2025-10-27,307.8000,313.5000,302.8800,313.0900,9868151
|
||||
2025-10-24,283.7700,310.7500,282.2100,307.4600,16914243
|
||||
2025-10-23,264.9500,285.5791,263.5623,285.0000,16676394
|
||||
2025-10-22,281.9900,289.1700,281.3500,287.5100,10538480
|
||||
2025-10-21,283.3100,285.3100,281.6000,282.0500,4080981
|
||||
2025-10-20,281.2500,285.5000,280.9600,283.6500,3494336
|
||||
2025-10-17,276.1500,283.4000,275.3500,281.2800,5309565
|
||||
2025-10-16,281.1100,282.5600,275.6000,275.9700,2956923
|
||||
2025-10-15,278.3800,285.4500,277.0000,280.7500,3346753
|
||||
2025-10-14,275.5200,277.5300,272.5469,276.1500,3058149
|
||||
2025-10-13,279.7900,282.4399,274.6400,277.2200,4333836
|
||||
2025-10-10,288.9700,290.3850,277.5000,277.8200,4508506
|
||||
2025-10-09,289.8200,290.1300,283.3200,288.2300,4912375
|
||||
2025-10-08,294.1600,294.2000,286.4730,289.4600,5297030
|
||||
2025-10-07,295.5500,301.0425,293.2850,293.8700,7190126
|
||||
2025-10-06,288.6100,291.4500,287.8000,289.4200,2881947
|
||||
2025-10-03,287.5000,293.3200,287.3000,288.3700,4375082
|
||||
2025-10-02,285.7900,288.5400,282.7900,286.7200,3814232
|
||||
2025-10-01,280.2000,286.5900,280.1500,286.4900,4381338
|
||||
2025-09-30,280.8800,286.0250,280.5200,282.1600,5926924
|
||||
2025-09-29,286.0000,286.0000,279.6600,279.8000,6022125
|
||||
2025-09-26,280.5100,288.8500,280.1100,284.3100,9063938
|
||||
2025-09-25,272.9350,284.2300,271.1480,281.4400,11506192
|
||||
2025-09-24,272.6200,273.6499,267.3000,267.5300,3159924
|
||||
2025-09-23,272.7000,273.2962,269.2650,272.2400,5394121
|
||||
2025-09-22,266.6200,272.3100,266.0000,271.3700,5030540
|
||||
2025-09-19,266.0500,267.8700,263.6400,266.4000,9858112
|
||||
2025-09-18,258.8600,265.2300,256.8004,265.0000,4988421
|
||||
2025-09-17,257.4950,260.9644,257.0100,259.0800,3974785
|
||||
2025-09-16,256.2600,258.0000,254.4100,257.5200,2719918
|
||||
2025-09-15,254.0200,259.0500,254.0000,256.2400,4028365
|
||||
2025-09-12,256.9500,257.2500,252.4250,253.4400,3433300
|
||||
2025-09-11,257.5600,258.5450,255.6550,257.0100,3576048
|
||||
2025-09-10,259.6500,260.0800,254.5600,256.8800,5185420
|
||||
2025-09-09,256.1200,260.6600,254.8800,259.1100,4931105
|
||||
2025-09-08,248.6300,257.1500,247.0200,256.0900,6940270
|
||||
2025-09-05,248.2300,249.0300,245.4500,248.5300,3147478
|
||||
2025-09-04,245.4200,249.2800,242.8500,247.1800,4765087
|
||||
2025-09-03,240.0200,244.2500,239.4100,244.1000,3156289
|
||||
2025-09-02,240.9000,241.5500,238.2500,241.5000,3469501
|
||||
2025-08-29,245.2300,245.4599,241.7200,243.4900,2967558
|
||||
2025-08-28,245.4300,245.8800,243.3600,245.7300,2820817
|
||||
2025-08-27,242.8700,245.9600,242.0000,244.8400,3698372
|
||||
2025-08-26,241.0200,244.9800,240.3800,242.6300,5386582
|
||||
2025-08-25,242.5650,242.5650,239.4300,239.4300,3513327
|
||||
2025-08-22,240.7400,243.6800,240.2200,242.0900,3134882
|
||||
2025-08-21,242.2100,242.5000,238.6500,239.4000,2991902
|
||||
2025-08-20,242.1100,242.8800,240.3400,242.5500,3240064
|
||||
2025-08-19,240.0000,242.8300,239.4900,241.2800,3328305
|
||||
2025-08-18,239.5700,241.4200,239.1158,239.4500,3569594
|
||||
2025-08-15,237.6100,240.6200,236.7700,239.7200,4344322
|
||||
2025-08-14,238.2500,239.0000,235.6200,237.1100,4556725
|
||||
2025-08-13,236.2000,240.8411,236.2000,240.0700,5663562
|
||||
2025-08-12,236.5300,237.9600,233.3600,234.7700,8800597
|
||||
2025-08-11,242.2400,243.1500,234.7000,236.3000,9381960
|
||||
2025-08-08,248.8800,249.4800,241.6500,242.2700,6828390
|
||||
2025-08-07,252.8100,255.0000,248.8750,250.1600,6251285
|
||||
2025-08-06,251.5300,254.3200,249.2800,252.2800,3692105
|
||||
2025-08-05,252.0000,252.8000,248.9950,250.6700,5823016
|
||||
2025-08-04,251.0500,252.0800,248.1100,251.9800,5280588
|
||||
2025-08-01,251.4050,251.4791,245.6100,250.0500,9683404
|
||||
2025-07-31,259.5700,259.9900,252.2200,253.1500,6739092
|
||||
2025-07-30,261.6000,262.0000,258.9000,260.2600,3718290
|
||||
2025-07-29,264.3000,265.7999,261.0200,262.4100,4627265
|
||||
2025-07-28,260.3000,264.0000,259.6100,263.2100,5192516
|
||||
2025-07-25,260.0200,260.8000,256.3500,259.7200,7758653
|
||||
2025-07-24,261.2500,262.0486,252.7500,260.5100,22647720
|
||||
2025-07-23,284.3000,288.0800,281.4400,282.0100,8105906
|
||||
2025-07-22,284.7400,284.8800,281.2500,281.9600,4824219
|
||||
2025-07-21,286.2900,287.7300,284.3800,284.7100,3051791
|
||||
2025-07-18,283.3800,287.1600,282.2200,285.8700,4478165
|
||||
2025-07-17,281.5000,283.4566,280.9000,282.0000,3337168
|
||||
2025-07-16,282.7500,283.8700,279.8700,281.9200,2804831
|
||||
2025-07-15,283.7700,284.1550,280.7301,282.7000,2864106
|
||||
2025-07-14,282.8300,284.9250,281.7100,283.7900,2857401
|
||||
2025-07-11,285.0100,287.4300,282.9200,283.5900,3790679
|
||||
2025-07-10,288.9000,288.9000,282.2100,287.4300,3489068
|
||||
2025-07-09,291.3900,291.6000,288.6300,290.1400,2971309
|
||||
2025-07-08,293.1000,295.6100,289.4900,290.4200,2925329
|
||||
timestamp,open,high,low,close,volume
|
||||
2025-11-25,304.1250,306.0000,297.0600,304.4800,2825322
|
||||
2025-11-24,299.1800,307.1800,297.5100,304.1200,6050640
|
||||
2025-11-21,293.4800,300.4800,291.8900,297.4400,5710903
|
||||
2025-11-20,294.6400,300.7100,290.1600,290.4000,5597028
|
||||
2025-11-19,290.5000,291.1099,288.0700,288.5300,3595912
|
||||
2025-11-18,297.0000,297.0000,289.9200,289.9500,4861928
|
||||
2025-11-17,305.5900,306.0000,296.5100,297.1700,3909741
|
||||
2025-11-14,300.0000,307.7200,297.5900,305.6900,3592455
|
||||
2025-11-13,312.2900,314.6000,303.6800,304.8600,5310150
|
||||
2025-11-12,319.8900,324.9000,314.5324,314.9800,6042686
|
||||
2025-11-11,309.0000,317.9100,308.4300,313.7200,4381913
|
||||
2025-11-10,306.8200,309.9400,304.2300,309.1300,2975188
|
||||
2025-11-07,309.6800,310.0000,302.6301,306.3800,5070773
|
||||
2025-11-06,306.7500,315.4400,301.0900,312.4200,6818521
|
||||
2025-11-05,301.3800,307.2000,299.7100,306.7700,4633195
|
||||
2025-11-04,300.0000,303.1700,296.0000,300.8500,5677330
|
||||
2025-11-03,308.0000,312.1411,304.2300,304.7300,4957958
|
||||
2025-10-31,312.0000,313.5000,301.6300,307.4100,7697499
|
||||
2025-10-30,306.6500,313.7500,305.0200,310.0600,4694275
|
||||
2025-10-29,312.7900,314.3300,307.5200,308.2100,4135948
|
||||
2025-10-28,312.6000,319.3500,311.4100,312.5700,6044770
|
||||
2025-10-27,307.8000,313.5000,302.8800,313.0900,9868151
|
||||
2025-10-24,283.7700,310.7500,282.2100,307.4600,16914243
|
||||
2025-10-23,264.9500,285.5791,263.5623,285.0000,16676394
|
||||
2025-10-22,281.9900,289.1700,281.3500,287.5100,10538480
|
||||
2025-10-21,283.3100,285.3100,281.6000,282.0500,4080981
|
||||
2025-10-20,281.2500,285.5000,280.9600,283.6500,3494336
|
||||
2025-10-17,276.1500,283.4000,275.3500,281.2800,5309565
|
||||
2025-10-16,281.1100,282.5600,275.6000,275.9700,2956923
|
||||
2025-10-15,278.3800,285.4500,277.0000,280.7500,3346753
|
||||
2025-10-14,275.5200,277.5300,272.5469,276.1500,3058149
|
||||
2025-10-13,279.7900,282.4399,274.6400,277.2200,4333836
|
||||
2025-10-10,288.9700,290.3850,277.5000,277.8200,4508506
|
||||
2025-10-09,289.8200,290.1300,283.3200,288.2300,4912375
|
||||
2025-10-08,294.1600,294.2000,286.4730,289.4600,5297030
|
||||
2025-10-07,295.5500,301.0425,293.2850,293.8700,7190126
|
||||
2025-10-06,288.6100,291.4500,287.8000,289.4200,2881947
|
||||
2025-10-03,287.5000,293.3200,287.3000,288.3700,4375082
|
||||
2025-10-02,285.7900,288.5400,282.7900,286.7200,3814232
|
||||
2025-10-01,280.2000,286.5900,280.1500,286.4900,4381338
|
||||
2025-09-30,280.8800,286.0250,280.5200,282.1600,5926924
|
||||
2025-09-29,286.0000,286.0000,279.6600,279.8000,6022125
|
||||
2025-09-26,280.5100,288.8500,280.1100,284.3100,9063938
|
||||
2025-09-25,272.9350,284.2300,271.1480,281.4400,11506192
|
||||
2025-09-24,272.6200,273.6499,267.3000,267.5300,3159924
|
||||
2025-09-23,272.7000,273.2962,269.2650,272.2400,5394121
|
||||
2025-09-22,266.6200,272.3100,266.0000,271.3700,5030540
|
||||
2025-09-19,266.0500,267.8700,263.6400,266.4000,9858112
|
||||
2025-09-18,258.8600,265.2300,256.8004,265.0000,4988421
|
||||
2025-09-17,257.4950,260.9644,257.0100,259.0800,3974785
|
||||
2025-09-16,256.2600,258.0000,254.4100,257.5200,2719918
|
||||
2025-09-15,254.0200,259.0500,254.0000,256.2400,4028365
|
||||
2025-09-12,256.9500,257.2500,252.4250,253.4400,3433300
|
||||
2025-09-11,257.5600,258.5450,255.6550,257.0100,3576048
|
||||
2025-09-10,259.6500,260.0800,254.5600,256.8800,5185420
|
||||
2025-09-09,256.1200,260.6600,254.8800,259.1100,4931105
|
||||
2025-09-08,248.6300,257.1500,247.0200,256.0900,6940270
|
||||
2025-09-05,248.2300,249.0300,245.4500,248.5300,3147478
|
||||
2025-09-04,245.4200,249.2800,242.8500,247.1800,4765087
|
||||
2025-09-03,240.0200,244.2500,239.4100,244.1000,3156289
|
||||
2025-09-02,240.9000,241.5500,238.2500,241.5000,3469501
|
||||
2025-08-29,245.2300,245.4599,241.7200,243.4900,2967558
|
||||
2025-08-28,245.4300,245.8800,243.3600,245.7300,2820817
|
||||
2025-08-27,242.8700,245.9600,242.0000,244.8400,3698372
|
||||
2025-08-26,241.0200,244.9800,240.3800,242.6300,5386582
|
||||
2025-08-25,242.5650,242.5650,239.4300,239.4300,3513327
|
||||
2025-08-22,240.7400,243.6800,240.2200,242.0900,3134882
|
||||
2025-08-21,242.2100,242.5000,238.6500,239.4000,2991902
|
||||
2025-08-20,242.1100,242.8800,240.3400,242.5500,3240064
|
||||
2025-08-19,240.0000,242.8300,239.4900,241.2800,3328305
|
||||
2025-08-18,239.5700,241.4200,239.1158,239.4500,3569594
|
||||
2025-08-15,237.6100,240.6200,236.7700,239.7200,4344322
|
||||
2025-08-14,238.2500,239.0000,235.6200,237.1100,4556725
|
||||
2025-08-13,236.2000,240.8411,236.2000,240.0700,5663562
|
||||
2025-08-12,236.5300,237.9600,233.3600,234.7700,8800597
|
||||
2025-08-11,242.2400,243.1500,234.7000,236.3000,9381960
|
||||
2025-08-08,248.8800,249.4800,241.6500,242.2700,6828390
|
||||
2025-08-07,252.8100,255.0000,248.8750,250.1600,6251285
|
||||
2025-08-06,251.5300,254.3200,249.2800,252.2800,3692105
|
||||
2025-08-05,252.0000,252.8000,248.9950,250.6700,5823016
|
||||
2025-08-04,251.0500,252.0800,248.1100,251.9800,5280588
|
||||
2025-08-01,251.4050,251.4791,245.6100,250.0500,9683404
|
||||
2025-07-31,259.5700,259.9900,252.2200,253.1500,6739092
|
||||
2025-07-30,261.6000,262.0000,258.9000,260.2600,3718290
|
||||
2025-07-29,264.3000,265.7999,261.0200,262.4100,4627265
|
||||
2025-07-28,260.3000,264.0000,259.6100,263.2100,5192516
|
||||
2025-07-25,260.0200,260.8000,256.3500,259.7200,7758653
|
||||
2025-07-24,261.2500,262.0486,252.7500,260.5100,22647720
|
||||
2025-07-23,284.3000,288.0800,281.4400,282.0100,8105906
|
||||
2025-07-22,284.7400,284.8800,281.2500,281.9600,4824219
|
||||
2025-07-21,286.2900,287.7300,284.3800,284.7100,3051791
|
||||
2025-07-18,283.3800,287.1600,282.2200,285.8700,4478165
|
||||
2025-07-17,281.5000,283.4566,280.9000,282.0000,3337168
|
||||
2025-07-16,282.7500,283.8700,279.8700,281.9200,2804831
|
||||
2025-07-15,283.7700,284.1550,280.7301,282.7000,2864106
|
||||
2025-07-14,282.8300,284.9250,281.7100,283.7900,2857401
|
||||
2025-07-11,285.0100,287.4300,282.9200,283.5900,3790679
|
||||
2025-07-10,288.9000,288.9000,282.2100,287.4300,3489068
|
||||
2025-07-09,291.3900,291.6000,288.6300,290.1400,2971309
|
||||
2025-07-08,293.1000,295.6100,289.4900,290.4200,2925329
|
||||
|
||||
|
+67
-67
@@ -1,67 +1,67 @@
|
||||
# GBM Class
|
||||
|
||||
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
|
||||
- **Configurable Parameters**: Control drift (trend) and volatility (noise).
|
||||
- **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
|
||||
- **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
|
||||
- **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
|
||||
|
||||
## Mathematical Model
|
||||
|
||||
The price evolution follows the stochastic differential equation:
|
||||
|
||||
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
|
||||
|
||||
Where:
|
||||
- $S_t$: Asset price at time $t$
|
||||
- $\mu$: Drift (expected return)
|
||||
- $\sigma$: Volatility (standard deviation of returns)
|
||||
- $W_t$: Wiener process (Brownian motion)
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class GBM : IFeed
|
||||
{
|
||||
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
|
||||
|
||||
public TBar Next(bool isNew = true);
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Initialization
|
||||
```csharp
|
||||
// Default: Start at 100, 5% drift, 20% volatility
|
||||
var gbm = new GBM();
|
||||
|
||||
// Custom: Start at 50, 10% drift, 50% volatility
|
||||
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
|
||||
```
|
||||
|
||||
### 2. Streaming Generation
|
||||
```csharp
|
||||
// Generate a new bar
|
||||
var bar = gbm.Next(isNew: true);
|
||||
|
||||
// Simulate intra-bar updates (e.g., real-time ticks)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var updatedBar = gbm.Next(isNew: false);
|
||||
Console.WriteLine($"Update: {updatedBar.Close}");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Batch Generation
|
||||
```csharp
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
// Generate 1000 bars
|
||||
var history = gbm.Fetch(1000, startTime, interval);
|
||||
# GBM Class
|
||||
|
||||
`GBM` (Geometric Brownian Motion) is a synthetic data generator that simulates realistic financial price movements. It is useful for testing indicators, strategies, and system performance without relying on external data files.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Geometric Brownian Motion**: Uses the standard mathematical model for asset price dynamics.
|
||||
- **Configurable Parameters**: Control drift (trend) and volatility (noise).
|
||||
- **Stateless Design**: Minimal memory footprint; only maintains state needed for continuity.
|
||||
- **Dual Modes**: Supports both streaming (bar-by-bar) and batch generation.
|
||||
- **Intra-bar Updates**: Can simulate real-time price updates within a single bar.
|
||||
|
||||
## Mathematical Model
|
||||
|
||||
The price evolution follows the stochastic differential equation:
|
||||
|
||||
$$ dS_t = \mu S_t dt + \sigma S_t dW_t $$
|
||||
|
||||
Where:
|
||||
- $S_t$: Asset price at time $t$
|
||||
- $\mu$: Drift (expected return)
|
||||
- $\sigma$: Volatility (standard deviation of returns)
|
||||
- $W_t$: Wiener process (Brownian motion)
|
||||
|
||||
## Class Definition
|
||||
|
||||
```csharp
|
||||
public class GBM : IFeed
|
||||
{
|
||||
public GBM(double startPrice = 100.0, double mu = 0.05, double sigma = 0.2, TimeSpan? defaultTimeframe = null);
|
||||
|
||||
public TBar Next(bool isNew = true);
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Initialization
|
||||
```csharp
|
||||
// Default: Start at 100, 5% drift, 20% volatility
|
||||
var gbm = new GBM();
|
||||
|
||||
// Custom: Start at 50, 10% drift, 50% volatility
|
||||
var volatileGbm = new GBM(startPrice: 50.0, mu: 0.10, sigma: 0.50);
|
||||
```
|
||||
|
||||
### 2. Streaming Generation
|
||||
```csharp
|
||||
// Generate a new bar
|
||||
var bar = gbm.Next(isNew: true);
|
||||
|
||||
// Simulate intra-bar updates (e.g., real-time ticks)
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
var updatedBar = gbm.Next(isNew: false);
|
||||
Console.WriteLine($"Update: {updatedBar.Close}");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Batch Generation
|
||||
```csharp
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
// Generate 1000 bars
|
||||
var history = gbm.Fetch(1000, startTime, interval);
|
||||
|
||||
+283
-283
@@ -1,283 +1,283 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GBMTests
|
||||
{
|
||||
[Fact]
|
||||
public void Next_DefaultParameter_GeneratesNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewTrue_AdvancesToNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
var bar2 = gbm.Next(isNew: true);
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
|
||||
var bar2 = gbm.Next(isNew: false);
|
||||
|
||||
Assert.Equal(initialTime, bar2.Time);
|
||||
// Price likely changed (GBM random walk)
|
||||
Assert.NotEqual(bar1.Close, bar2.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_RefBool_HonorsRequest()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// GBM always honors isNew - parameter should remain unchanged
|
||||
bool isNew1 = true;
|
||||
var bar1 = gbm.Next(ref isNew1);
|
||||
Assert.True(isNew1, "GBM should honor isNew=true request");
|
||||
|
||||
bool isNew2 = false;
|
||||
long time1 = bar1.Time;
|
||||
var bar2 = gbm.Next(ref isNew2);
|
||||
Assert.False(isNew2, "GBM should honor isNew=false request");
|
||||
Assert.Equal(time1, bar2.Time);
|
||||
|
||||
bool isNew3 = true;
|
||||
var bar3 = gbm.Next(ref isNew3);
|
||||
Assert.True(isNew3, "GBM should honor isNew=true request");
|
||||
Assert.NotEqual(time1, bar3.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesCorrectCount()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
int count = 10;
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(count, startTime, interval);
|
||||
|
||||
Assert.Equal(count, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesSequentialBars()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify time sequence
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
Assert.True(series[i].Time > series[i - 1].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_RespectsInterval()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var interval = TimeSpan.FromHours(1);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify interval spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_StartsAtSpecifiedTime()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.Equal(startTime, series[0].Time);
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time);
|
||||
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithDifferentIntervals_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Test different intervals
|
||||
var intervals = new[] {
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
foreach (var interval in intervals)
|
||||
{
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
// Verify spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesRealisticOHLCV()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(10, startTime, interval);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var bar = series[i];
|
||||
|
||||
// High should be >= max(Open, Close)
|
||||
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
|
||||
|
||||
// Low should be <= min(Open, Close)
|
||||
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
|
||||
|
||||
// Volume should be positive
|
||||
Assert.True(bar.Volume > 0);
|
||||
|
||||
// All prices should be positive
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High > 0);
|
||||
Assert.True(bar.Low > 0);
|
||||
Assert.True(bar.Close > 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntraBarUpdates_ModifyCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
double initialClose = bar1.Close;
|
||||
|
||||
// Loop until price changes (random walk might stay same but unlikely)
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
Assert.Equal(initialTime, bar.Time);
|
||||
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(changed, "Price should change during intra-bar updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedStreamingAndBatch_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Start with streaming
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// Batch generation with explicit time
|
||||
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.True(series[0].Time > bar2.Time);
|
||||
Assert.Equal(3, series.Count);
|
||||
|
||||
// Continue streaming after batch (uses internal state)
|
||||
var bar3 = gbm.Next();
|
||||
Assert.True(bar3.Time > series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriftAndVolatility_AffectPriceMovement()
|
||||
{
|
||||
// High volatility should produce more price variation
|
||||
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
|
||||
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
|
||||
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
|
||||
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
|
||||
|
||||
// Calculate price ranges
|
||||
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
|
||||
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
|
||||
|
||||
// High volatility should generally produce larger absolute movements
|
||||
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsecutiveCalls_MaintainContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// bar2.Open should equal bar1.Close (continuity)
|
||||
Assert.Equal(bar1.Close, bar2.Open);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stateless_NoHistoryStorage()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Generate multiple bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
gbm.Next();
|
||||
}
|
||||
|
||||
// GBM should not expose any history storage
|
||||
var type = gbm.GetType();
|
||||
var barsProperty = type.GetProperty("Bars");
|
||||
|
||||
Assert.Null(barsProperty);
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using Xunit;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class GBMTests
|
||||
{
|
||||
[Fact]
|
||||
public void Next_DefaultParameter_GeneratesNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewTrue_AdvancesToNewBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
var bar2 = gbm.Next(isNew: true);
|
||||
|
||||
Assert.NotEqual(bar1.Time, bar2.Time);
|
||||
Assert.True(bar2.Time > bar1.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_IsNewFalse_UpdatesCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
|
||||
var bar2 = gbm.Next(isNew: false);
|
||||
|
||||
Assert.Equal(initialTime, bar2.Time);
|
||||
// Price likely changed (GBM random walk)
|
||||
Assert.NotEqual(bar1.Close, bar2.Close);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Next_RefBool_HonorsRequest()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// GBM always honors isNew - parameter should remain unchanged
|
||||
bool isNew1 = true;
|
||||
var bar1 = gbm.Next(ref isNew1);
|
||||
Assert.True(isNew1, "GBM should honor isNew=true request");
|
||||
|
||||
bool isNew2 = false;
|
||||
long time1 = bar1.Time;
|
||||
var bar2 = gbm.Next(ref isNew2);
|
||||
Assert.False(isNew2, "GBM should honor isNew=false request");
|
||||
Assert.Equal(time1, bar2.Time);
|
||||
|
||||
bool isNew3 = true;
|
||||
var bar3 = gbm.Next(ref isNew3);
|
||||
Assert.True(isNew3, "GBM should honor isNew=true request");
|
||||
Assert.NotEqual(time1, bar3.Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesCorrectCount()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
int count = 10;
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(count, startTime, interval);
|
||||
|
||||
Assert.Equal(count, series.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_GeneratesSequentialBars()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify time sequence
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
Assert.True(series[i].Time > series[i - 1].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_RespectsInterval()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var interval = TimeSpan.FromHours(1);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
var series = gbm.Fetch(5, startTime, interval);
|
||||
|
||||
// Verify interval spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_StartsAtSpecifiedTime()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
var startTime = new DateTime(2024, 1, 1, 9, 30, 0, DateTimeKind.Utc).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.Equal(startTime, series[0].Time);
|
||||
Assert.Equal(startTime + interval.Ticks, series[1].Time);
|
||||
Assert.Equal(startTime + 2 * interval.Ticks, series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fetch_WithDifferentIntervals_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
// Test different intervals
|
||||
var intervals = new[] {
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(5),
|
||||
TimeSpan.FromHours(1)
|
||||
};
|
||||
|
||||
foreach (var interval in intervals)
|
||||
{
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
// Verify spacing
|
||||
for (int i = 1; i < series.Count; i++)
|
||||
{
|
||||
long expectedDiff = interval.Ticks;
|
||||
long actualDiff = series[i].Time - series[i - 1].Time;
|
||||
Assert.Equal(expectedDiff, actualDiff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesRealisticOHLCV()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(10, startTime, interval);
|
||||
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
var bar = series[i];
|
||||
|
||||
// High should be >= max(Open, Close)
|
||||
Assert.True(bar.High >= Math.Max(bar.Open, bar.Close));
|
||||
|
||||
// Low should be <= min(Open, Close)
|
||||
Assert.True(bar.Low <= Math.Min(bar.Open, bar.Close));
|
||||
|
||||
// Volume should be positive
|
||||
Assert.True(bar.Volume > 0);
|
||||
|
||||
// All prices should be positive
|
||||
Assert.True(bar.Open > 0);
|
||||
Assert.True(bar.High > 0);
|
||||
Assert.True(bar.Low > 0);
|
||||
Assert.True(bar.Close > 0);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IntraBarUpdates_ModifyCurrentBar()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next(isNew: true);
|
||||
long initialTime = bar1.Time;
|
||||
double initialClose = bar1.Close;
|
||||
|
||||
// Loop until price changes (random walk might stay same but unlikely)
|
||||
bool changed = false;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
Assert.Equal(initialTime, bar.Time);
|
||||
if (Math.Abs(bar.Close - initialClose) > double.Epsilon)
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Assert.True(changed, "Price should change during intra-bar updates");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MixedStreamingAndBatch_WorksCorrectly()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Start with streaming
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// Batch generation with explicit time
|
||||
long startTime = bar2.Time + TimeSpan.FromMinutes(1).Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var series = gbm.Fetch(3, startTime, interval);
|
||||
|
||||
Assert.True(series[0].Time > bar2.Time);
|
||||
Assert.Equal(3, series.Count);
|
||||
|
||||
// Continue streaming after batch (uses internal state)
|
||||
var bar3 = gbm.Next();
|
||||
Assert.True(bar3.Time > series[2].Time);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DriftAndVolatility_AffectPriceMovement()
|
||||
{
|
||||
// High volatility should produce more price variation
|
||||
var gbmLowVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.01);
|
||||
var gbmHighVol = new GBM(startPrice: 100.0, mu: 0.0, sigma: 0.5);
|
||||
|
||||
long startTime = DateTime.UtcNow.Ticks;
|
||||
var interval = TimeSpan.FromMinutes(1);
|
||||
var seriesLow = gbmLowVol.Fetch(100, startTime, interval);
|
||||
var seriesHigh = gbmHighVol.Fetch(100, startTime, interval);
|
||||
|
||||
// Calculate price ranges
|
||||
double rangeLow = seriesLow[99].Close - seriesLow[0].Open;
|
||||
double rangeHigh = seriesHigh[99].Close - seriesHigh[0].Open;
|
||||
|
||||
// High volatility should generally produce larger absolute movements
|
||||
Assert.True(Math.Abs(rangeHigh) > Math.Abs(rangeLow) * 0.5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConsecutiveCalls_MaintainContinuity()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
var bar1 = gbm.Next();
|
||||
var bar2 = gbm.Next();
|
||||
|
||||
// bar2.Open should equal bar1.Close (continuity)
|
||||
Assert.Equal(bar1.Close, bar2.Open);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Stateless_NoHistoryStorage()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100.0);
|
||||
|
||||
// Generate multiple bars
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
gbm.Next();
|
||||
}
|
||||
|
||||
// GBM should not expose any history storage
|
||||
var type = gbm.GetType();
|
||||
var barsProperty = type.GetProperty("Bars");
|
||||
|
||||
Assert.Null(barsProperty);
|
||||
}
|
||||
}
|
||||
|
||||
+211
-211
@@ -1,211 +1,211 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
|
||||
/// Generates realistic price data for testing indicators and strategies.
|
||||
/// Stateless design - only maintains minimal state needed for price continuity.
|
||||
/// </summary>
|
||||
public class GBM : IFeed
|
||||
{
|
||||
private readonly Random _rnd = new();
|
||||
|
||||
private double _lastPrice;
|
||||
private long _lastTime;
|
||||
|
||||
private readonly double _mu;
|
||||
private readonly double _sigma;
|
||||
private readonly double _dt;
|
||||
|
||||
// Precomputed GBM constants
|
||||
private readonly double _drift;
|
||||
private readonly double _vol;
|
||||
private readonly long _defaultTimeStep;
|
||||
|
||||
// State for streaming bar formation (only when isNew=false)
|
||||
private TBar _currentBar;
|
||||
private bool _hasCurrentBar;
|
||||
|
||||
// Box-Muller optimization: cache second normal
|
||||
private double _cachedZ;
|
||||
private bool _hasCachedZ;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GBM generator.
|
||||
/// </summary>
|
||||
/// <param name="startPrice">Initial price (default: 100.0)</param>
|
||||
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
|
||||
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
|
||||
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
|
||||
public GBM(
|
||||
double startPrice = 100.0,
|
||||
double mu = 0.05,
|
||||
double sigma = 0.2,
|
||||
TimeSpan? defaultTimeframe = null)
|
||||
{
|
||||
_lastPrice = startPrice;
|
||||
_lastTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
_mu = mu;
|
||||
_sigma = sigma;
|
||||
|
||||
// Use provided timeframe or default to 1 minute
|
||||
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
|
||||
_defaultTimeStep = timeframe.Ticks;
|
||||
|
||||
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
_dt = timeframe.TotalMinutes / minutesPerYear;
|
||||
|
||||
_drift = (mu - 0.5 * sigma * sigma) * _dt;
|
||||
_vol = sigma * Math.Sqrt(_dt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates next standard normal using Box-Muller transform with caching.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double NextNormal()
|
||||
{
|
||||
if (_hasCachedZ)
|
||||
{
|
||||
_hasCachedZ = false;
|
||||
return _cachedZ;
|
||||
}
|
||||
|
||||
double u1 = 1.0 - _rnd.NextDouble();
|
||||
double u2 = 1.0 - _rnd.NextDouble();
|
||||
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
|
||||
double angle = 2.0 * Math.PI * u2;
|
||||
|
||||
_cachedZ = mag * Math.Sin(angle);
|
||||
_hasCachedZ = true;
|
||||
|
||||
return mag * Math.Cos(angle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with full bidirectional control.
|
||||
/// GBM always honors the request - isNew parameter unchanged on return.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(ref bool isNew)
|
||||
{
|
||||
// GBM always honors request - parameter unchanged
|
||||
|
||||
if (isNew || !_hasCurrentBar)
|
||||
{
|
||||
// Generate new bar
|
||||
long currentTime = _lastTime + _defaultTimeStep;
|
||||
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
double open = _lastPrice;
|
||||
double close = price;
|
||||
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
|
||||
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
|
||||
|
||||
_currentBar = new TBar(currentTime, open, high, low, close, volume);
|
||||
_hasCurrentBar = true;
|
||||
|
||||
_lastPrice = close;
|
||||
_lastTime = currentTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update current bar (intra-bar tick)
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
var bar = _currentBar;
|
||||
double newClose = price;
|
||||
double newHigh = Math.Max(bar.High, newClose);
|
||||
double newLow = Math.Min(bar.Low, newClose);
|
||||
|
||||
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
|
||||
_lastPrice = newClose;
|
||||
}
|
||||
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with simple control.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(bool isNew = true)
|
||||
{
|
||||
// Delegate to ref version
|
||||
return Next(ref isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be positive", nameof(count));
|
||||
|
||||
var series = new TBarSeries(count);
|
||||
|
||||
// Pre-allocate arrays for SoA layout
|
||||
long[] t = new long[count];
|
||||
double[] o = new double[count];
|
||||
double[] h = new double[count];
|
||||
double[] l = new double[count];
|
||||
double[] c = new double[count];
|
||||
double[] v = new double[count];
|
||||
|
||||
// Calculate dt for this specific interval
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
double dt = interval.TotalMinutes / minutesPerYear;
|
||||
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
|
||||
double vol = _sigma * Math.Sqrt(dt);
|
||||
|
||||
long timeStep = interval.Ticks;
|
||||
double currentPrice = _lastPrice;
|
||||
long currentTime = startTime;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double z = NextNormal();
|
||||
double price = currentPrice * Math.Exp(drift + vol * z);
|
||||
|
||||
double open = currentPrice;
|
||||
double close = price;
|
||||
|
||||
double rnd1 = _rnd.NextDouble();
|
||||
double rnd2 = _rnd.NextDouble();
|
||||
double rnd3 = _rnd.NextDouble();
|
||||
|
||||
t[i] = currentTime;
|
||||
o[i] = open;
|
||||
c[i] = close;
|
||||
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
|
||||
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
|
||||
v[i] = 1000 + rnd3 * 1000;
|
||||
|
||||
currentPrice = price;
|
||||
currentTime += timeStep;
|
||||
}
|
||||
|
||||
// Update internal state to continue from end of batch
|
||||
_lastPrice = currentPrice;
|
||||
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
|
||||
|
||||
// Bulk add to series
|
||||
series.Add(t, o, h, l, c, v);
|
||||
|
||||
// Reset streaming state after batch
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Geometric Brownian Motion (GBM) generator for simulating OHLCV data.
|
||||
/// Generates realistic price data for testing indicators and strategies.
|
||||
/// Stateless design - only maintains minimal state needed for price continuity.
|
||||
/// </summary>
|
||||
public class GBM : IFeed
|
||||
{
|
||||
private readonly Random _rnd = new();
|
||||
|
||||
private double _lastPrice;
|
||||
private long _lastTime;
|
||||
|
||||
private readonly double _mu;
|
||||
private readonly double _sigma;
|
||||
private readonly double _dt;
|
||||
|
||||
// Precomputed GBM constants
|
||||
private readonly double _drift;
|
||||
private readonly double _vol;
|
||||
private readonly long _defaultTimeStep;
|
||||
|
||||
// State for streaming bar formation (only when isNew=false)
|
||||
private TBar _currentBar;
|
||||
private bool _hasCurrentBar;
|
||||
|
||||
// Box-Muller optimization: cache second normal
|
||||
private double _cachedZ;
|
||||
private bool _hasCachedZ;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new GBM generator.
|
||||
/// </summary>
|
||||
/// <param name="startPrice">Initial price (default: 100.0)</param>
|
||||
/// <param name="mu">Annual drift/return rate (default: 0.05 = 5%)</param>
|
||||
/// <param name="sigma">Annual volatility (default: 0.2 = 20%)</param>
|
||||
/// <param name="defaultTimeframe">Default timeframe for bars (default: 1 minute)</param>
|
||||
public GBM(
|
||||
double startPrice = 100.0,
|
||||
double mu = 0.05,
|
||||
double sigma = 0.2,
|
||||
TimeSpan? defaultTimeframe = null)
|
||||
{
|
||||
_lastPrice = startPrice;
|
||||
_lastTime = DateTime.UtcNow.Ticks;
|
||||
|
||||
_mu = mu;
|
||||
_sigma = sigma;
|
||||
|
||||
// Use provided timeframe or default to 1 minute
|
||||
var timeframe = defaultTimeframe ?? TimeSpan.FromMinutes(1);
|
||||
_defaultTimeStep = timeframe.Ticks;
|
||||
|
||||
// Calculate dt based on timeframe (assuming 252 trading days/year, 6.5 hours/day)
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
_dt = timeframe.TotalMinutes / minutesPerYear;
|
||||
|
||||
_drift = (mu - 0.5 * sigma * sigma) * _dt;
|
||||
_vol = sigma * Math.Sqrt(_dt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates next standard normal using Box-Muller transform with caching.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double NextNormal()
|
||||
{
|
||||
if (_hasCachedZ)
|
||||
{
|
||||
_hasCachedZ = false;
|
||||
return _cachedZ;
|
||||
}
|
||||
|
||||
double u1 = 1.0 - _rnd.NextDouble();
|
||||
double u2 = 1.0 - _rnd.NextDouble();
|
||||
double mag = Math.Sqrt(-2.0 * Math.Log(u1));
|
||||
double angle = 2.0 * Math.PI * u2;
|
||||
|
||||
_cachedZ = mag * Math.Sin(angle);
|
||||
_hasCachedZ = true;
|
||||
|
||||
return mag * Math.Cos(angle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with full bidirectional control.
|
||||
/// GBM always honors the request - isNew parameter unchanged on return.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(ref bool isNew)
|
||||
{
|
||||
// GBM always honors request - parameter unchanged
|
||||
|
||||
if (isNew || !_hasCurrentBar)
|
||||
{
|
||||
// Generate new bar
|
||||
long currentTime = _lastTime + _defaultTimeStep;
|
||||
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
double open = _lastPrice;
|
||||
double close = price;
|
||||
double high = Math.Max(open, close) * (1.0 + _rnd.NextDouble() * 0.01);
|
||||
double low = Math.Min(open, close) * (1.0 - _rnd.NextDouble() * 0.01);
|
||||
|
||||
_currentBar = new TBar(currentTime, open, high, low, close, volume);
|
||||
_hasCurrentBar = true;
|
||||
|
||||
_lastPrice = close;
|
||||
_lastTime = currentTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update current bar (intra-bar tick)
|
||||
double z = NextNormal();
|
||||
double price = _lastPrice * Math.Exp(_drift + _vol * z);
|
||||
double volume = 1000 + _rnd.NextDouble() * 1000;
|
||||
|
||||
var bar = _currentBar;
|
||||
double newClose = price;
|
||||
double newHigh = Math.Max(bar.High, newClose);
|
||||
double newLow = Math.Min(bar.Low, newClose);
|
||||
|
||||
_currentBar = new TBar(bar.Time, bar.Open, newHigh, newLow, newClose, volume);
|
||||
_lastPrice = newClose;
|
||||
}
|
||||
|
||||
return _currentBar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the next bar with simple control.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBar Next(bool isNew = true)
|
||||
{
|
||||
// Delegate to ref version
|
||||
return Next(ref isNew);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TBarSeries Fetch(int count, long startTime, TimeSpan interval)
|
||||
{
|
||||
if (count <= 0)
|
||||
throw new ArgumentException("Count must be positive", nameof(count));
|
||||
|
||||
var series = new TBarSeries(count);
|
||||
|
||||
// Pre-allocate arrays for SoA layout
|
||||
long[] t = new long[count];
|
||||
double[] o = new double[count];
|
||||
double[] h = new double[count];
|
||||
double[] l = new double[count];
|
||||
double[] c = new double[count];
|
||||
double[] v = new double[count];
|
||||
|
||||
// Calculate dt for this specific interval
|
||||
double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||
double dt = interval.TotalMinutes / minutesPerYear;
|
||||
double drift = (_mu - 0.5 * _sigma * _sigma) * dt;
|
||||
double vol = _sigma * Math.Sqrt(dt);
|
||||
|
||||
long timeStep = interval.Ticks;
|
||||
double currentPrice = _lastPrice;
|
||||
long currentTime = startTime;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double z = NextNormal();
|
||||
double price = currentPrice * Math.Exp(drift + vol * z);
|
||||
|
||||
double open = currentPrice;
|
||||
double close = price;
|
||||
|
||||
double rnd1 = _rnd.NextDouble();
|
||||
double rnd2 = _rnd.NextDouble();
|
||||
double rnd3 = _rnd.NextDouble();
|
||||
|
||||
t[i] = currentTime;
|
||||
o[i] = open;
|
||||
c[i] = close;
|
||||
h[i] = Math.Max(open, close) * (1.0 + rnd1 * 0.01);
|
||||
l[i] = Math.Min(open, close) * (1.0 - rnd2 * 0.01);
|
||||
v[i] = 1000 + rnd3 * 1000;
|
||||
|
||||
currentPrice = price;
|
||||
currentTime += timeStep;
|
||||
}
|
||||
|
||||
// Update internal state to continue from end of batch
|
||||
_lastPrice = currentPrice;
|
||||
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
|
||||
|
||||
// Bulk add to series
|
||||
series.Add(t, o, h, l, c, v);
|
||||
|
||||
// Reset streaming state after batch
|
||||
_hasCurrentBar = false;
|
||||
|
||||
return series;
|
||||
}
|
||||
}
|
||||
|
||||
+51
-51
@@ -1,51 +1,51 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<Title>QuanTAlib</Title>
|
||||
<Product>Library of TA Calculations, Charts and Strategies for Quantower</Product>
|
||||
<Description>Quantitative Technical Analysis Library in C# for Quantower</Description>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/mihakralj/QuanTAlib</RepositoryUrl>
|
||||
<Authors>Miha Kralj</Authors>
|
||||
<Copyright>Miha Kralj</Copyright>
|
||||
<RootNamespace>QuanTAlib</RootNamespace>
|
||||
<AssemblyName>QuanTAlib</AssemblyName>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageIcon>QuanTAlib2.png</PackageIcon>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||
<PackageTags>
|
||||
Indicators;Stock;Market;Technical;Analysis;Algorithmic;Trading;Trade;Trend;Momentum;Finance;Algorithm;Algo;
|
||||
AlgoTrading;Financial;Strategy;Chart;Charting;Oscillator;Overlay;Equity;Bitcoin;Crypto;Cryptocurrency;Forex;
|
||||
Quantitative;Historical;Quotes;
|
||||
</PackageTags>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/mihakralj/QuanTAlib/main/.github/QuanTAlib2.png</PackageIconUrl>
|
||||
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<Version>$(GitVersion_MajorMinorPatch)</Version>
|
||||
<PackageVersion>$(GitVersion_MajorMinorPatch)</PackageVersion>
|
||||
<AssemblyVersion>$(GitVersion_AssemblySemVer)</AssemblyVersion>
|
||||
<FileVersion>$(GitVersion_AssemblySemFileVer)</FileVersion>
|
||||
<InformationalVersion>$(GitVersion_InformationalVersion)</InformationalVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<UserSecretsId>6afc11a7-4355-4f5e-9fdf-22431e5b03cb</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.cs" Exclude="**\*.Tests.cs;**\*.Quantower.cs;obj\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.github\QuanTAlib2.png" Pack="true" Visible="false" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<Title>QuanTAlib</Title>
|
||||
<Product>Library of TA Calculations, Charts and Strategies for Quantower</Product>
|
||||
<Description>Quantitative Technical Analysis Library in C# for Quantower</Description>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/mihakralj/QuanTAlib</RepositoryUrl>
|
||||
<Authors>Miha Kralj</Authors>
|
||||
<Copyright>Miha Kralj</Copyright>
|
||||
<RootNamespace>QuanTAlib</RootNamespace>
|
||||
<AssemblyName>QuanTAlib</AssemblyName>
|
||||
<IsPublishable>true</IsPublishable>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageIcon>QuanTAlib2.png</PackageIcon>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||
<PackageTags>
|
||||
Indicators;Stock;Market;Technical;Analysis;Algorithmic;Trading;Trade;Trend;Momentum;Finance;Algorithm;Algo;
|
||||
AlgoTrading;Financial;Strategy;Chart;Charting;Oscillator;Overlay;Equity;Bitcoin;Crypto;Cryptocurrency;Forex;
|
||||
Quantitative;Historical;Quotes;
|
||||
</PackageTags>
|
||||
<PackageIconUrl>https://raw.githubusercontent.com/mihakralj/QuanTAlib/main/.github/QuanTAlib2.png</PackageIconUrl>
|
||||
<EnforceCodeStyleInBuild>True</EnforceCodeStyleInBuild>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<Version>$(GitVersion_MajorMinorPatch)</Version>
|
||||
<PackageVersion>$(GitVersion_MajorMinorPatch)</PackageVersion>
|
||||
<AssemblyVersion>$(GitVersion_AssemblySemVer)</AssemblyVersion>
|
||||
<FileVersion>$(GitVersion_AssemblySemFileVer)</FileVersion>
|
||||
<InformationalVersion>$(GitVersion_InformationalVersion)</InformationalVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<UserSecretsId>6afc11a7-4355-4f5e-9fdf-22431e5b03cb</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="**\*.cs" Exclude="**\*.Tests.cs;**\*.Quantower.cs;obj\**\*.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.github\QuanTAlib2.png" Pack="true" Visible="false" PackagePath="" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+32
-32
@@ -1,32 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>Averages</AssemblyName>
|
||||
<AlgoType>Indicator</AlgoType>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="*.cs" />
|
||||
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
|
||||
<Compile Include="..\lib\averages\**\*.cs" Exclude="..\lib\averages\**\*.Tests.cs" />
|
||||
<Reference Include="TradingPlatform.BusinessLayer">
|
||||
<HintPath>..\.github\TradingPlatform.BusinessLayer.dll</HintPath>
|
||||
</Reference>
|
||||
<None Include="..\.github\TradingPlatform.BusinessLayer.xml">
|
||||
<Link>TradingPlatform.BusinessLayer.xml</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyCustomContent" AfterTargets="AfterBuild" Condition="'$(IsLocalBuild)' == 'true' AND $([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<Copy SourceFiles="$(OutputPath)\Averages.dll" DestinationFolder="$(QuantowerRoot)\Settings\Scripts\Indicators\QuanTAlib\Averages" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<AssemblyName>Averages</AssemblyName>
|
||||
<AlgoType>Indicator</AlgoType>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="*.cs" />
|
||||
<Compile Include="..\lib\core\**\*.cs" Exclude="..\lib\core\**\*.Tests.cs" />
|
||||
<Compile Include="..\lib\averages\**\*.cs" Exclude="..\lib\averages\**\*.Tests.cs" />
|
||||
<Reference Include="TradingPlatform.BusinessLayer">
|
||||
<HintPath>..\.github\TradingPlatform.BusinessLayer.dll</HintPath>
|
||||
</Reference>
|
||||
<None Include="..\.github\TradingPlatform.BusinessLayer.xml">
|
||||
<Link>TradingPlatform.BusinessLayer.xml</Link>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyCustomContent" AfterTargets="AfterBuild" Condition="'$(IsLocalBuild)' == 'true' AND $([MSBuild]::IsOSPlatform('Windows'))">
|
||||
<Copy SourceFiles="$(OutputPath)\Averages.dll" DestinationFolder="$(QuantowerRoot)\Settings\Scripts\Indicators\QuanTAlib\Averages" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
+237
-237
@@ -1,237 +1,237 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
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 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 = (int)converter.GetChartY(value);
|
||||
|
||||
using (pen)
|
||||
{
|
||||
gr.DrawLine(pen, new Point(leftX, Y), new Point(rightX, Y));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
|
||||
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
List<Point> allPoints = new List<Point>();
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(indicator.HistoricalData.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);
|
||||
}
|
||||
|
||||
if (allPoints.Count > 1)
|
||||
{
|
||||
if (allPoints.Count < 2) return;
|
||||
|
||||
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 = indicator.Count - warmupPeriod - rightIndex;
|
||||
|
||||
// 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 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);
|
||||
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
|
||||
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(indicator.HistoricalData.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)
|
||||
{
|
||||
using (Brush hist = new SolidBrush(Color.FromArgb(150, 0, 255, 0)))
|
||||
{
|
||||
gr.FillRectangle(hist, barX, barY, HistBarWidth, Math.Abs(barY - barY0));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Brush hist = new SolidBrush(Color.FromArgb(150, 255, 0, 0)))
|
||||
{
|
||||
gr.FillRectangle(hist, barX, barY0, HistBarWidth, Math.Abs(barY0 - barY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
|
||||
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 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 = (int)converter.GetChartY(value);
|
||||
|
||||
using (pen)
|
||||
{
|
||||
gr.DrawLine(pen, new Point(leftX, Y), new Point(rightX, Y));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
|
||||
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
List<Point> allPoints = new List<Point>();
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(indicator.HistoricalData.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);
|
||||
}
|
||||
|
||||
if (allPoints.Count > 1)
|
||||
{
|
||||
if (allPoints.Count < 2) return;
|
||||
|
||||
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 = indicator.Count - warmupPeriod - rightIndex;
|
||||
|
||||
// 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 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);
|
||||
DateTime leftTime = new[] { converter.GetTime(clientRect.Left), indicator.HistoricalData.Time(indicator!.Count - 1) }.Max();
|
||||
DateTime rightTime = new[] { converter.GetTime(clientRect.Right), indicator.HistoricalData.Time(0) }.Min();
|
||||
|
||||
int leftIndex = (int)indicator.HistoricalData.GetIndexByTime(leftTime.Ticks) + 1;
|
||||
int rightIndex = (int)indicator.HistoricalData.GetIndexByTime(rightTime.Ticks);
|
||||
|
||||
for (int i = rightIndex; i < leftIndex; i++)
|
||||
{
|
||||
int barX = (int)converter.GetChartX(indicator.HistoricalData.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)
|
||||
{
|
||||
using (Brush hist = new SolidBrush(Color.FromArgb(150, 0, 255, 0)))
|
||||
{
|
||||
gr.FillRectangle(hist, barX, barY, HistBarWidth, Math.Abs(barY - barY0));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using (Brush hist = new SolidBrush(Color.FromArgb(150, 255, 0, 0)))
|
||||
{
|
||||
gr.FillRectangle(hist, barX, barY0, HistBarWidth, Math.Abs(barY0 - barY));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user