diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 00000000..e69f298e --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +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: + - src \ No newline at end of file diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index bab332b0..4293fa8b 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -1,17 +1,37 @@ name: Publish Workflow on: - push: # Triggers on push events to any branch - pull_request: # Triggers on pull request events targeting any branch - workflow_dispatch: # Allows manual triggering of the workflow + 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 @@ -22,7 +42,20 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + 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 @@ -61,6 +94,7 @@ jobs: /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" ` @@ -68,6 +102,8 @@ jobs: /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 @@ -75,8 +111,15 @@ jobs: 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" @@ -86,9 +129,21 @@ jobs: - name: Generate Coverage Report run: | - reportgenerator -reports:*cover*.xml -targetdir:. + 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 }} @@ -102,13 +157,19 @@ jobs: coverage-reports: '*cover*.xml' - name: Upload Coverage to Codecov - uses: codecov/codecov-action@v3 + 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 @@ -118,12 +179,22 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + 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: latest - name: Restore dependencies run: dotnet restore @@ -131,63 +202,51 @@ jobs: - name: Build run: dotnet build --no-restore --configuration Debug - - name: Run Codacy Analysis CLI - uses: codacy/codacy-analysis-cli-action@v4 - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - verbose: true - output: results.sarif - format: sarif - gh-code-scanning-compat: true - max-allowed-issues: 2147483647 - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 - - - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 with: - sarif_file: results.sarif + output: results + upload: true - SecurityCodeScan: - runs-on: windows-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 + - 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: - fetch-depth: 0 + args: | + --file=./lib/quantalib.csproj + --severity-threshold=low + --detection-depth=4 + --package-manager=nuget - - name: Setup NuGet - uses: nuget/setup-nuget@v1 - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v1 - - - name: Setup .NET SDK - uses: actions/setup-dotnet@v4 + - 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: - dotnet-version: | - 8.x - 3.1.x - dotnet-quality: 'preview' - - - name: Set up projects for analysis - uses: security-code-scan/security-code-scan-add-action@v1 - - - name: Build - run: | - dotnet restore - dotnet build --no-restore --configuration Debug - - - name: Convert SARIF for uploading to GitHub - uses: security-code-scan/security-code-scan-results-action@v1 - - - name: Upload SARIF - uses: github/codeql-action/upload-sarif@v3 + 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: - needs: [Code_Coverage, CodeQL, SecurityCodeScan] + timeout-minutes: 20 + needs: [Code_Coverage, CodeQL] if: | success() && (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) || @@ -202,7 +261,14 @@ jobs: - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.x' + 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: Install GitVersion uses: gittools/actions/gitversion/setup@v0 @@ -226,8 +292,6 @@ jobs: dotnet build ./quantower/Volatility/_Volatility.csproj --configuration Release --nologo dotnet build ./SyntheticVendor/SyntheticVendor.csproj --configuration Release --nologo -############# Publish dev release - - name: Create or Update Development Release if: github.ref == 'refs/heads/dev' env: @@ -247,16 +311,16 @@ jobs: - 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 }} - -############## Publish main release + --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' }} + if: github.ref == 'refs/heads/main' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/Directory.Build.props b/Directory.Build.props index a5cfd53c..a6c4ab71 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -8,7 +8,6 @@ true en-US false - false true AnyCPU True @@ -21,6 +20,9 @@ snupkg AnyCPU true + true + true + true @@ -49,6 +51,7 @@ + diff --git a/SyntheticVendor/SyntheticVendor.cs b/SyntheticVendor/SyntheticVendor.cs index 90898bae..89033c94 100644 --- a/SyntheticVendor/SyntheticVendor.cs +++ b/SyntheticVendor/SyntheticVendor.cs @@ -597,25 +597,6 @@ public class SyntheticVendor : Vendor }; } - private HistoryItemBar GeneratePulseWave(DateTime time, TimeSpan slice) - { - double hours = (time - DateTime.UnixEpoch).TotalHours; - double period = 24; // 24-hour period - double position = hours % period; - double value = position < period / 5 ? 100 : -100; // 20% duty cycle - - return new HistoryItemBar - { - TicksLeft = time.Ticks, - TicksRight = time.Add(slice).Ticks - 1, - Open = value, - High = 100, - Low = -100, - Close = value, - Volume = 100, - Ticks = 100 - }; - } private HistoryItemBar GenerateTriangleWave(DateTime time, TimeSpan slice) { diff --git a/Tests/test_updates_statistics.cs b/Tests/test_updates_statistics.cs index d1820474..a5d901fc 100644 --- a/Tests/test_updates_statistics.cs +++ b/Tests/test_updates_statistics.cs @@ -17,6 +17,24 @@ public class StatisticsUpdateTests return ((double)BitConverter.ToUInt64(bytes, 0) / ulong.MaxValue * 200) - 100; // Range: -100 to 100 } + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + (high - low) * GetRandomDouble(); + return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); + } + + private TBar GetRandomBar(bool IsNew) + { + double open = GetRandomDouble(); + double high = open + Math.Abs(GetRandomDouble()); + double low = open - Math.Abs(GetRandomDouble()); + double close = low + (high - low) * GetRandomDouble(); + return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); + } + [Fact] public void Curvature_Update() { @@ -47,6 +65,22 @@ public class StatisticsUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Hurst_Update() + { + var indicator = new Hurst(period: 100, minLength: 10); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Kurtosis_Update() { diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index c48a2d9e..26b6a5f9 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -26,6 +26,22 @@ public class VolatilityUpdateTests return new TBar(DateTime.Now, open, high, low, close, 1000, IsNew); } + [Fact] + public void Adr_Update() + { + var indicator = new Adr(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Atr_Update() { @@ -42,6 +58,166 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Ap_Update() + { + var indicator = new Ap(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Atrp_Update() + { + var indicator = new Atrp(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Bband_Update() + { + var indicator = new Bband(period: 20, multiplier: 2.0); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Ccv_Update() + { + var indicator = new Ccv(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Ce_Update() + { + var indicator = new Ce(period: 22, multiplier: 3.0); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cv_Update() + { + var indicator = new Cv(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Cvi_Update() + { + var indicator = new Cvi(period: 10, smoothPeriod: 10); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Ewma_Update() + { + var indicator = new Ewma(period: 20, lambda: 0.94); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Fcb_Update() + { + var indicator = new Fcb(period: 20, smoothing: 0.5); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Gkv_Update() + { + var indicator = new Gkv(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Historical_Update() { @@ -57,6 +233,22 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + [Fact] + public void Hlv_Update() + { + var indicator = new Hlv(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + [Fact] public void Jvolty_Update() { @@ -102,22 +294,6 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } - [Fact] - public void Cvi_Update() - { - var indicator = new Cvi(period: 14); - TBar r = GetRandomBar(true); - double initialValue = indicator.Calc(r); - - for (int i = 0; i < RandomUpdates; i++) - { - indicator.Calc(GetRandomBar(IsNew: false)); - } - double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); - - Assert.Equal(initialValue, finalValue, precision); - } - [Fact] public void Tr_Update() { diff --git a/Tests/test_updates_volume.cs b/Tests/test_updates_volume.cs index 166a5326..c85a619b 100644 --- a/Tests/test_updates_volume.cs +++ b/Tests/test_updates_volume.cs @@ -319,4 +319,68 @@ public class VolumeUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Vf_Update() + { + var indicator = new Vf(period: 13); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vp_Update() + { + var indicator = new Vp(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vwap_Update() + { + var indicator = new Vwap(); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } + + [Fact] + public void Vwma_Update() + { + var indicator = new Vwma(period: 20); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/docs/indicators/indicators.md b/docs/indicators/indicators.md index 4d97d32c..0aecedff 100644 --- a/docs/indicators/indicators.md +++ b/docs/indicators/indicators.md @@ -1,16 +1,18 @@ # Indicators in QuanTAlib + + | **Category** | **Status** | **Completion** | |--------------|:----------:|:--------------:| | Basic Transforms | 6 of 6 | 100% | | Averages & Trends | 33 of 33 | 100% | | Momentum | 17 of 17 | 100% | -| Oscillators | 12 of 29 | 41% | -| Volatility | 15 of 35 | 43% | -| Volume | 19 of 19 | 100% | -| Numerical Analysis | 13 of 20 | 65% | +| Oscillators | 11 of 29 | 38% | +| Volatility | 24 of 35 | 69% | +| Volume | 15 of 19 | 79% | +| Numerical Analysis | 13 of 19 | 68% | | Errors | 16 of 16 | 100% | -| **Total** | **131 of 175** | **75%** | +| **Total** | **135 of 174** | **78%** | |Technical Indicator Name| Class Name| |-----------|:----------:| @@ -109,16 +111,16 @@ |ATR - Average True Range|`Atr`| |ATRP - Average True Range Percent|`Atrp`| |ATRS - ATR Trailing Stop|`Atrs`| -|🚧 BB* - Bollinger Bands® (Upper, Middle, Lower)|`Bb`| -|🚧 CCV - Close-to-Close Volatility|`Ccv`| -|🚧 CE - Chandelier Exit|`Ce`| -|🚧 CV - Conditional Volatility (ARCH/GARCH)|`Cv`| -|🚧 CVI - Chaikin's Volatility|`Cvi`| +|BB* - Bollinger Bands® (Upper, Middle, Lower)|`Bb`| +|CCV - Close-to-Close Volatility|`Ccv`| +|CE - Chandelier Exit|`Ce`| +|CV - Conditional Volatility (ARCH/GARCH)|`Cv`| +|CVI - Chaikin's Volatility|`Cvi`| |🚧 DC* - Donchian Channels (Upper, Middle, Lower)|`Dc`| -|🚧 EWMA - Exponential Weighted Moving Average Volatility|`Ewma`| -|🚧 FCB - Fractal Chaos Bands|`Fcb`| -|🚧 GKV - Garman-Klass Volatility|`Gkv`| -|🚧 HLV - High-Low Volatility|`Hlv`| +|EWMA - Exponential Weighted Moving Average Volatility|`Ewma`| +|FCB - Fractal Chaos Bands|`Fcb`| +|GKV - Garman-Klass Volatility|`Gkv`| +|HLV - High-Low Volatility|`Hlv`| |HV - Historical Volatility|`Hv`| |🚧 ICH* - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span)|`Ich`| |JVOLTY - Jurik Volatility|`Jvolty`| diff --git a/lib/averages/Dema.cs b/lib/averages/Dema.cs index f70ae547..3f543649 100644 --- a/lib/averages/Dema.cs +++ b/lib/averages/Dema.cs @@ -18,7 +18,6 @@ namespace QuanTAlib; /// public class Dema : AbstractBase { - private readonly int _period; private readonly double _k; private readonly double _epsilon = 1e-10; private double _lastEma1, _p_lastEma1; @@ -31,8 +30,7 @@ public class Dema : AbstractBase { throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } - _period = period; - _k = 2.0 / (_period + 1); + _k = 2.0 / (period + 1); Name = "Dema"; double percentile = 0.85; //targeting 85th percentile of correctness of converging EMA WarmupPeriod = (int)System.Math.Ceiling(-period * System.Math.Log(1 - percentile)); diff --git a/lib/averages/Dsma.cs b/lib/averages/Dsma.cs index 7720fb7b..238a78f7 100644 --- a/lib/averages/Dsma.cs +++ b/lib/averages/Dsma.cs @@ -21,9 +21,8 @@ namespace QuanTAlib; /// public class Dsma : AbstractBase { - private readonly int _period; private readonly CircularBuffer _buffer; - private readonly double _c1, _c2, _c3; + private readonly double _c2, _c3; private readonly double _scaleFactor; private readonly double _periodRecip; // 1/_period private readonly double _scaleByPeriod; // 5/_period @@ -49,7 +48,6 @@ public class Dsma : AbstractBase { throw new ArgumentOutOfRangeException(nameof(scaleFactor), "Scale factor must be between 0 and 1 (exclusive)."); } - _period = period; _periodRecip = 1.0 / period; _scaleFactor = scaleFactor; _buffer = new CircularBuffer(period); @@ -61,7 +59,7 @@ public class Dsma : AbstractBase _c2 = b1; _c3 = -a1 * a1; - _c1 = 1.0 - _c2 - _c3; + double _c1 = 1.0 - _c2 - _c3; _c1Half = _c1 * 0.5; _scaleByPeriod = 5.0 / period; diff --git a/lib/averages/Epma.cs b/lib/averages/Epma.cs index 9fca0906..6cad88c7 100644 --- a/lib/averages/Epma.cs +++ b/lib/averages/Epma.cs @@ -26,7 +26,6 @@ public class Epma : AbstractBase { private readonly int _period; private readonly Convolution _convolution; - private readonly double[] _baseKernel; /// The number of data points used in the EPMA calculation. /// Thrown when period is less than 1. @@ -37,7 +36,7 @@ public class Epma : AbstractBase throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _baseKernel = GenerateKernel(_period); + double[] _baseKernel = GenerateKernel(_period); _convolution = new Convolution(_baseKernel); Name = "Epma"; WarmupPeriod = period; diff --git a/lib/averages/Fwma.cs b/lib/averages/Fwma.cs index b1a7a8c0..eb8f2618 100644 --- a/lib/averages/Fwma.cs +++ b/lib/averages/Fwma.cs @@ -26,7 +26,6 @@ namespace QuanTAlib; public class Fwma : AbstractBase { private readonly Convolution _convolution; - private readonly double[] _kernel; /// The number of data points used in the FWMA calculation. /// Thrown when period is less than 1. @@ -36,7 +35,7 @@ public class Fwma : AbstractBase { throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _kernel = GenerateKernel(period); + double[] _kernel = GenerateKernel(period); _convolution = new Convolution(_kernel); Name = "Fwma"; WarmupPeriod = period; diff --git a/lib/averages/Gma.cs b/lib/averages/Gma.cs index 4924ae2c..c0828318 100644 --- a/lib/averages/Gma.cs +++ b/lib/averages/Gma.cs @@ -26,7 +26,6 @@ namespace QuanTAlib; public class Gma : AbstractBase { private readonly Convolution _convolution; - private readonly double[] _kernel; /// The number of data points used in the GMA calculation. /// Thrown when period is less than 1. @@ -36,7 +35,7 @@ public class Gma : AbstractBase { throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _kernel = GenerateKernel(period); + double[] _kernel = GenerateKernel(period); _convolution = new Convolution(_kernel); Name = "Gma"; WarmupPeriod = period; diff --git a/lib/averages/Hma.cs b/lib/averages/Hma.cs index 1fe049e7..d36c9f34 100644 --- a/lib/averages/Hma.cs +++ b/lib/averages/Hma.cs @@ -28,11 +28,6 @@ namespace QuanTAlib; public class Hma : AbstractBase { private readonly Convolution _wmaHalf, _wmaFull, _wmaFinal; - private readonly int _period; - private readonly int _sqrtPeriod; - private readonly double[] _kernelHalf; - private readonly double[] _kernelFull; - private readonly double[] _kernelFinal; /// The number of data points used in the HMA calculation. Must be at least 2. /// Thrown when period is less than 2. @@ -42,13 +37,12 @@ public class Hma : AbstractBase { throw new System.ArgumentException("Period must be greater than or equal to 2.", nameof(period)); } - _period = period; - _sqrtPeriod = (int)System.Math.Sqrt(period); + int _sqrtPeriod = (int)System.Math.Sqrt(period); // Generate all kernels once - _kernelHalf = GenerateWmaKernel(period / 2); - _kernelFull = GenerateWmaKernel(period); - _kernelFinal = GenerateWmaKernel(_sqrtPeriod); + double[] _kernelHalf = GenerateWmaKernel(period / 2); + double[] _kernelFull = GenerateWmaKernel(period); + double[] _kernelFinal = GenerateWmaKernel(_sqrtPeriod); // Initialize convolutions with pre-generated kernels _wmaHalf = new Convolution(_kernelHalf); diff --git a/lib/averages/Sinema.cs b/lib/averages/Sinema.cs index 34fa800e..10f0f4ab 100644 --- a/lib/averages/Sinema.cs +++ b/lib/averages/Sinema.cs @@ -28,7 +28,6 @@ namespace QuanTAlib; public class Sinema : AbstractBase { private readonly Convolution _convolution; - private readonly double[] _kernel; /// The number of data points used in the SINEMA calculation. /// Thrown when period is less than 1. @@ -38,7 +37,7 @@ public class Sinema : AbstractBase { throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } - _kernel = GenerateKernel(period); + double[] _kernel = GenerateKernel(period); _convolution = new Convolution(_kernel); Name = "Sinema"; WarmupPeriod = period; diff --git a/lib/averages/Sma.cs b/lib/averages/Sma.cs index f6dec4bb..fb267e31 100644 --- a/lib/averages/Sma.cs +++ b/lib/averages/Sma.cs @@ -27,7 +27,6 @@ namespace QuanTAlib; public class Sma : AbstractBase { private readonly CircularBuffer _buffer; - private readonly int _period; /// The number of data points used in the SMA calculation. /// Thrown when period is less than 1. @@ -37,7 +36,6 @@ public class Sma : AbstractBase { throw new System.ArgumentOutOfRangeException(nameof(period), "Period must be greater than or equal to 1."); } - _period = period; _buffer = new CircularBuffer(period); Name = "Sma"; WarmupPeriod = period; diff --git a/lib/averages/T3.cs b/lib/averages/T3.cs index 7f65090e..e7db51bf 100644 --- a/lib/averages/T3.cs +++ b/lib/averages/T3.cs @@ -28,7 +28,6 @@ namespace QuanTAlib; public class T3 : AbstractBase { private readonly int _period; - private readonly double _vfactor; private readonly bool _useSma; private readonly double _k; private readonly double _c1, _c2, _c3, _c4; @@ -48,7 +47,6 @@ public class T3 : AbstractBase throw new System.ArgumentException("Period must be greater than or equal to 1.", nameof(period)); } _period = period; - _vfactor = vfactor; _useSma = useSma; WarmupPeriod = period; @@ -69,7 +67,7 @@ public class T3 : AbstractBase _buffer5 = new(period); _buffer6 = new(period); - Name = $"T3({_period}, {_vfactor})"; + Name = $"T3({_period}, {vfactor})"; Init(); } diff --git a/lib/quantalib.csproj b/lib/quantalib.csproj index 5b87dfa0..a5bae0db 100644 --- a/lib/quantalib.csproj +++ b/lib/quantalib.csproj @@ -1,36 +1,27 @@ - QuanTAlib Library of TA Calculations, Charts and Strategies for Quantower Quantitative Technical Analysis Library in C# for Quantower git https://github.com/mihakralj/QuanTAlib - true Miha Kralj Miha Kralj readme.md QuanTAlib QuanTAlib - 0.0.0.0 True - AnyCPU - full - True True QuanTAlib2.png - readme.md Apache-2.0 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; - QuanTAlib2.png https://raw.githubusercontent.com/mihakralj/QuanTAlib/main/.github/QuanTAlib2.png True false - $(NoWarn);NU1903;NU5104 @@ -51,4 +42,4 @@ - + \ No newline at end of file diff --git a/lib/statistics/Hurst.cs b/lib/statistics/Hurst.cs new file mode 100644 index 00000000..4ec62254 --- /dev/null +++ b/lib/statistics/Hurst.cs @@ -0,0 +1,194 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// HURST: Hurst Exponent +/// A measure of long-term memory of time series that relates to the +/// autocorrelations of the time series, and the rate at which these +/// decrease as the lag between pairs of values increases. +/// +/// +/// The Hurst Exponent calculation process: +/// 1. Calculate log returns of the series +/// 2. Create subsequences of different lengths +/// 3. For each length: +/// - Calculate range (max-min) of cumulative deviations +/// - Calculate standard deviation +/// - Calculate R/S ratio +/// 4. Fit log(R/S) vs log(length) to find H +/// +/// Key characteristics: +/// - H = 0.5: Random walk (Brownian motion) +/// - 0.5 < H ≤ 1.0: Trending (persistent) series +/// - 0 ≤ H < 0.5: Mean-reverting (anti-persistent) series +/// - Default minimum length is 10 +/// - Default maximum length is period/2 +/// +/// Formula: +/// R(n)/S(n) = c * n^H +/// where: +/// R(n) = range of cumulative deviations +/// S(n) = standard deviation +/// n = subsequence length +/// H = Hurst exponent +/// +/// Market Applications: +/// - Market efficiency analysis +/// - Trend strength measurement +/// - Trading strategy development +/// - Risk assessment +/// - Market regime identification +/// +/// Sources: +/// H.E. Hurst (1951) +/// "Long-term Storage Capacity of Reservoirs" +/// Transactions of the American Society of Civil Engineers, 116, 770-799 +/// +/// Note: Returns a value between 0 and 1 +/// + +[SkipLocalsInit] +public sealed class Hurst : AbstractBase +{ + private readonly int _period; + private readonly int _minLength; + private readonly CircularBuffer _prices; + private readonly CircularBuffer _logReturns; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hurst(int period = 100, int minLength = 10) + { + if (minLength < 10) + { + throw new ArgumentOutOfRangeException(nameof(minLength), "Minimum length must be at least 10."); + } + if (period <= minLength * 2) + { + throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least twice the minimum length."); + } + + _period = period; + _minLength = minLength; + WarmupPeriod = period + 1; // Need one extra period for returns + Name = $"HURST({_period})"; + _prices = new CircularBuffer(period); + _logReturns = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hurst(object source, int period = 100, int minLength = 10) : this(period, minLength) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prices.Clear(); + _logReturns.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + private static (double range, double stdDev) CalculateRangeAndStdDev(ReadOnlySpan data) + { + int n = data.Length; + if (n == 0) return (0, 0); + + // Calculate mean + double mean = 0; + for (int i = 0; i < n; i++) + { + mean += data[i]; + } + mean /= n; + + // Calculate cumulative deviations and std dev + double max = double.MinValue; + double min = double.MaxValue; + double sumSquaredDev = 0; + double cumDev = 0; + + for (int i = 0; i < n; i++) + { + double dev = data[i] - mean; + cumDev += dev; + max = Math.Max(max, cumDev); + min = Math.Min(min, cumDev); + sumSquaredDev += dev * dev; + } + + double range = max - min; + double stdDev = Math.Sqrt(sumSquaredDev / n); + + return (range, stdDev); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Add price and calculate log return + _prices.Add(BarInput.Close); + if (_index > 1) + { + double logReturn = Math.Log(BarInput.Close / _prices[1]); + _logReturns.Add(logReturn); + } + + // Need enough values for calculation + if (_index <= _period) + { + return 0.5; // Return random walk value until we have enough data + } + + // Calculate R/S values for different lengths + int maxLength = _period / 2; + int numPoints = 0; + double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0; + + for (int length = _minLength; length <= maxLength; length *= 2) + { + var (range, stdDev) = CalculateRangeAndStdDev(_logReturns.GetSpan()[..length]); + if (stdDev > 0) + { + double rs = range / stdDev; + if (rs > 0) + { + double x = Math.Log(length); + double y = Math.Log(rs); + sumX += x; + sumY += y; + sumXY += x * y; + sumX2 += x * x; + numPoints++; + } + } + } + + // Calculate Hurst exponent using linear regression + double hurst = 0.5; // Default to random walk + if (numPoints > 1) + { + double slope = (numPoints * sumXY - sumX * sumY) / (numPoints * sumX2 - sumX * sumX); + hurst = Math.Max(0, Math.Min(1, slope)); // Clamp between 0 and 1 + } + + IsHot = _index >= WarmupPeriod; + return hurst; + } +} diff --git a/lib/volatility/Bband.cs b/lib/volatility/Bband.cs new file mode 100644 index 00000000..a135b71b --- /dev/null +++ b/lib/volatility/Bband.cs @@ -0,0 +1,143 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// BBAND: Bollinger Bands® +/// A technical analysis tool that creates a band of three lines: +/// - Middle Band: n-period simple moving average (SMA) +/// - Upper Band: Middle Band + (standard deviation * multiplier) +/// - Lower Band: Middle Band - (standard deviation * multiplier) +/// +/// +/// The Bollinger Bands calculation process: +/// 1. Calculate the middle band (SMA of closing prices) +/// 2. Calculate the standard deviation of prices +/// 3. Upper and lower bands are the middle band +/- standard deviation * multiplier +/// +/// Key characteristics: +/// - Adapts to volatility +/// - Default period is 20 days +/// - Default multiplier is 2.0 +/// - Returns three bands (upper, middle, lower) +/// - Wider bands indicate higher volatility +/// - Narrower bands indicate lower volatility +/// +/// Formula: +/// Middle Band = SMA(Close, period) +/// Standard Deviation = SQRT(SUM((Close - Middle Band)^2) / period) +/// Upper Band = Middle Band + (multiplier * Standard Deviation) +/// Lower Band = Middle Band - (multiplier * Standard Deviation) +/// +/// Market Applications: +/// - Volatility measurement +/// - Overbought/oversold identification +/// - Price breakout detection +/// - Trend strength analysis +/// - Dynamic support/resistance levels +/// +/// Sources: +/// John Bollinger (1980s) +/// https://www.bollingerbands.com +/// +/// Note: Returns three values: upper, middle, and lower bands +/// + +[SkipLocalsInit] +public sealed class Bband : AbstractBase +{ + private readonly int _period; + private readonly double _multiplier; + private readonly CircularBuffer _prices; + private double _middleBand; + private double _upperBand; + private double _lowerBand; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bband(int period = 20, double multiplier = 2.0) + { + _period = period; + _multiplier = multiplier; + WarmupPeriod = period; + Name = $"BBAND({_period},{_multiplier})"; + _prices = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Bband(object source, int period = 20, double multiplier = 2.0) : this(period, multiplier) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _middleBand = 0; + _upperBand = 0; + _lowerBand = 0; + _prices.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Add current price to buffer + _prices.Add(BarInput.Close); + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate middle band (SMA) + _middleBand = _prices.Average(); + + // Calculate standard deviation + double sumSquaredDeviations = 0; + for (int i = 0; i < _period; i++) + { + double deviation = _prices[i] - _middleBand; + sumSquaredDeviations += deviation * deviation; + } + double standardDeviation = Math.Sqrt(sumSquaredDeviations / _period); + + // Calculate bands + double bandWidth = _multiplier * standardDeviation; + _upperBand = _middleBand + bandWidth; + _lowerBand = _middleBand - bandWidth; + + IsHot = _index >= WarmupPeriod; + return _middleBand; // Return middle band as primary value + } + + /// + /// Gets the upper band value + /// + public double UpperBand => _upperBand; + + /// + /// Gets the middle band value (SMA) + /// + public double MiddleBand => _middleBand; + + /// + /// Gets the lower band value + /// + public double LowerBand => _lowerBand; +} diff --git a/lib/volatility/Ccv.cs b/lib/volatility/Ccv.cs new file mode 100644 index 00000000..cfe6e7ba --- /dev/null +++ b/lib/volatility/Ccv.cs @@ -0,0 +1,130 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CCV: Close-to-Close Volatility +/// A measure of price volatility that uses only closing prices, +/// calculated as the standard deviation of logarithmic returns. +/// +/// +/// The CCV calculation process: +/// 1. Calculate logarithmic returns: ln(Close[t]/Close[t-1]) +/// 2. Calculate standard deviation of returns over the period +/// 3. Annualize by multiplying by sqrt(trading days per year) +/// +/// Key characteristics: +/// - Uses only closing prices +/// - Based on logarithmic returns +/// - Default period is 20 days +/// - Annualized by default (multiply by sqrt(252)) +/// - Expressed as a percentage +/// +/// Formula: +/// Returns = ln(Close[t]/Close[t-1]) +/// CCV = StdDev(Returns, period) * sqrt(252) * 100 +/// +/// Market Applications: +/// - Volatility measurement +/// - Risk assessment +/// - Option pricing +/// - Trading strategy development +/// - Portfolio management +/// +/// Sources: +/// Close-to-Close Volatility concept +/// https://www.investopedia.com/terms/v/volatility.asp +/// +/// Note: Returns annualized volatility as a percentage +/// + +[SkipLocalsInit] +public sealed class Ccv : AbstractBase +{ + private readonly int _period; + private readonly bool _annualize; + private readonly CircularBuffer _returns; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ccv(int period = 20, bool annualize = true) + { + _period = period; + _annualize = annualize; + WarmupPeriod = period + 1; // Need one extra period for returns calculation + Name = $"CCV({_period})"; + _returns = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ccv(object source, int period = 20, bool annualize = true) : this(period, annualize) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _returns.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate logarithmic return + double logReturn = Math.Log(BarInput.Close / _prevClose); + _returns.Add(logReturn); + _prevClose = BarInput.Close; + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate standard deviation + double mean = _returns.Average(); + double sumSquaredDeviations = 0; + for (int i = 0; i < _period; i++) + { + double deviation = _returns[i] - mean; + sumSquaredDeviations += deviation * deviation; + } + double stdDev = Math.Sqrt(sumSquaredDeviations / _period); + + // Annualize if requested (sqrt(252) for trading days in a year) + if (_annualize) + { + stdDev *= Math.Sqrt(252); + } + + // Convert to percentage + double volatility = stdDev * 100; + + IsHot = _index >= WarmupPeriod; + return volatility; + } +} diff --git a/lib/volatility/Ce.cs b/lib/volatility/Ce.cs new file mode 100644 index 00000000..225c2c22 --- /dev/null +++ b/lib/volatility/Ce.cs @@ -0,0 +1,157 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CE: Chandelier Exit +/// A volatility-based stop-loss indicator that adapts to market conditions, +/// using ATR to set stop levels above/below recent price extremes. +/// +/// +/// The CE calculation process: +/// 1. Calculate highest high and lowest low over the period +/// 2. Calculate ATR over the period +/// 3. Long Exit = Highest High - (ATR * multiplier) +/// 4. Short Exit = Lowest Low + (ATR * multiplier) +/// +/// Key characteristics: +/// - Adapts to market volatility +/// - Default period is 22 days +/// - Default multiplier is 3.0 +/// - Returns both long and short exit levels +/// - Based on ATR and price extremes +/// +/// Formula: +/// ATR = Average(TR, period) +/// Long Exit = Highest High[period] - (multiplier * ATR) +/// Short Exit = Lowest Low[period] + (multiplier * ATR) +/// +/// Market Applications: +/// - Stop loss placement +/// - Position management +/// - Trend following +/// - Risk control +/// - Exit strategy +/// +/// Sources: +/// Chuck LeBeau +/// https://www.investopedia.com/terms/c/chandelier-exit.asp +/// +/// Note: Returns two values: long exit and short exit levels +/// + +[SkipLocalsInit] +public sealed class Ce : AbstractBase +{ + private readonly int _period; + private readonly double _multiplier; + private readonly CircularBuffer _tr; + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private double _prevClose; + private double _longExit; + private double _shortExit; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ce(int period = 22, double multiplier = 3.0) + { + _period = period; + _multiplier = multiplier; + WarmupPeriod = period + 1; // Need one extra period for TR + Name = $"CE({_period},{_multiplier})"; + _tr = new CircularBuffer(period); + _highs = new CircularBuffer(period); + _lows = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ce(object source, int period = 22, double multiplier = 3.0) : this(period, multiplier) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _longExit = 0; + _shortExit = 0; + _tr.Clear(); + _highs.Clear(); + _lows.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate True Range + double tr = Math.Max(BarInput.High - BarInput.Low, + Math.Max(Math.Abs(BarInput.High - _prevClose), + Math.Abs(BarInput.Low - _prevClose))); + + // Add values to buffers + _tr.Add(tr); + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate ATR + double atr = _tr.Average(); + + // Find highest high and lowest low + double highestHigh = double.MinValue; + double lowestLow = double.MaxValue; + for (int i = 0; i < _period; i++) + { + highestHigh = Math.Max(highestHigh, _highs[i]); + lowestLow = Math.Min(lowestLow, _lows[i]); + } + + // Calculate exit levels + _longExit = highestHigh - (_multiplier * atr); + _shortExit = lowestLow + (_multiplier * atr); + + IsHot = _index >= WarmupPeriod; + return _longExit; // Return long exit as primary value + } + + /// + /// Gets the long exit level + /// + public double LongExit => _longExit; + + /// + /// Gets the short exit level + /// + public double ShortExit => _shortExit; +} diff --git a/lib/volatility/Cv.cs b/lib/volatility/Cv.cs new file mode 100644 index 00000000..77dd3fff --- /dev/null +++ b/lib/volatility/Cv.cs @@ -0,0 +1,138 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CV: Conditional Volatility (GARCH) +/// Implements the GARCH(1,1) model for estimating conditional volatility, +/// which captures volatility clustering and mean reversion in financial markets. +/// +/// +/// The CV (GARCH) calculation process: +/// 1. Calculate returns: (Close[t] - Close[t-1])/Close[t-1] +/// 2. Update variance estimate using GARCH(1,1) formula: +/// σ²[t] = ω + α*r²[t-1] + β*σ²[t-1] +/// 3. Take square root to get volatility +/// +/// Key characteristics: +/// - Captures volatility clustering +/// - Mean-reverting behavior +/// - Responds to market shocks +/// - Default period is 20 days +/// - Returns annualized volatility +/// +/// Formula: +/// Returns[t] = (Close[t] - Close[t-1])/Close[t-1] +/// σ²[t] = ω + α*Returns²[t-1] + β*σ²[t-1] +/// CV[t] = sqrt(σ²[t]) * sqrt(252) * 100 +/// +/// Where: +/// ω (omega) = long-term variance * (1 - α - β) +/// α (alpha) = weight of recent squared return +/// β (beta) = weight of previous variance +/// +/// Market Applications: +/// - Risk measurement +/// - Option pricing +/// - Value at Risk (VaR) +/// - Portfolio optimization +/// - Volatility forecasting +/// +/// Sources: +/// Bollerslev (1986) +/// https://en.wikipedia.org/wiki/GARCH +/// +/// Note: Returns annualized volatility as a percentage +/// + +[SkipLocalsInit] +public sealed class Cv : AbstractBase +{ + private readonly int _period; + private readonly double _alpha; + private readonly double _beta; + private readonly double _omega; + private double _prevClose; + private double _prevVariance; + private bool _isInitialized; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cv(int period = 20, double alpha = 0.1, double beta = 0.8) + { + _period = period; + _alpha = alpha; + _beta = beta; + _omega = 0.001 * (1 - alpha - beta); // Initial estimate, will be updated with actual data + WarmupPeriod = period + 1; // Need one extra period for returns + Name = $"CV({_period})"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cv(object source, int period = 20, double alpha = 0.1, double beta = 0.8) : this(period, alpha, beta) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _prevVariance = 0; + _isInitialized = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate return + double return_ = (BarInput.Close - _prevClose) / _prevClose; + double squaredReturn = return_ * return_; + _prevClose = BarInput.Close; + + // Initialize with first available data if not done + if (!_isInitialized && _index > _period) + { + double _longTermVariance = squaredReturn; // Use current squared return as initial estimate + _prevVariance = _longTermVariance; + _isInitialized = true; + } + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Update variance estimate using GARCH(1,1) + double variance = _omega + _alpha * squaredReturn + _beta * _prevVariance; + _prevVariance = variance; + + // Calculate annualized volatility as percentage + double volatility = Math.Sqrt(variance) * Math.Sqrt(252) * 100; + + IsHot = _index >= WarmupPeriod; + return volatility; + } +} diff --git a/lib/volatility/Cvi.cs b/lib/volatility/Cvi.cs index bc16cbbb..25c3912d 100644 --- a/lib/volatility/Cvi.cs +++ b/lib/volatility/Cvi.cs @@ -2,65 +2,63 @@ using System.Runtime.CompilerServices; namespace QuanTAlib; /// -/// CVI: Chaikin's Volatility -/// A technical indicator developed by Marc Chaikin that measures the volatility of a financial instrument by comparing the spread between the high and low prices. +/// CVI: Chaikin's Volatility Index +/// Measures the rate of change of a moving average of the difference +/// between high and low prices, indicating volatility expansion/contraction. /// /// /// The CVI calculation process: -/// 1. Calculates the difference between the high and low prices. -/// 2. Applies an exponential moving average (EMA) to the differences. -/// 3. Computes the percentage change in the EMA over a specified period. +/// 1. Calculate High-Low difference +/// 2. Take EMA of High-Low difference +/// 3. Calculate ROC of the EMA over specified period /// /// Key characteristics: -/// - Measures volatility -/// - Uses high and low prices -/// - Percentage-based -/// - EMA smoothing +/// - Measures volatility expansion/contraction +/// - Default period is 10 days +/// - Default smoothing period is 10 days +/// - Positive values indicate expanding volatility +/// - Negative values indicate contracting volatility /// /// Formula: -/// CVI = (EMA(high - low, period) - EMA(high - low, period, offset)) / EMA(high - low, period, offset) * 100 +/// HL = High - Low +/// Smoothed = EMA(HL, smoothPeriod) +/// CVI = ((Smoothed - Smoothed[period]) / Smoothed[period]) * 100 /// /// Market Applications: -/// - Volatility assessment -/// - Trend confirmation -/// - Risk management -/// - Entry/exit timing +/// - Volatility measurement +/// - Trend strength analysis +/// - Market regime identification +/// - Trading range analysis +/// - Breakout confirmation /// /// Sources: -/// Marc Chaikin - Original development -/// https://www.investopedia.com/terms/c/chaikins-volatility.asp +/// Marc Chaikin +/// https://www.investopedia.com/terms/c/chaikinvolatility.asp /// -/// Note: Higher CVI values indicate higher volatility +/// Note: Returns percentage change in volatility /// [SkipLocalsInit] public sealed class Cvi : AbstractBase { private readonly int _period; - private readonly Ema _ema; - private readonly CircularBuffer _buffer; - private double _prevEma; + private readonly CircularBuffer _smoothed; + private readonly double _alpha; + private double _ema; - /// The number of periods for CVI calculation. - /// Thrown when period is less than 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Cvi(int period) + public Cvi(int period = 10, int smoothPeriod = 10) { - if (period < 1) - { - throw new ArgumentOutOfRangeException(nameof(period), - "Period must be greater than or equal to 1."); - } _period = period; - _ema = new Ema(period); - _buffer = new CircularBuffer(period); - WarmupPeriod = period; - Name = $"CVI({period})"; + _alpha = 2.0 / (smoothPeriod + 1); + WarmupPeriod = _period + smoothPeriod; + Name = $"CVI({_period},{smoothPeriod})"; + _smoothed = new CircularBuffer(_period); + Init(); } /// The data source object that publishes updates. - /// The number of periods for CVI calculation. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Cvi(object source, int period) : this(period) + public Cvi(object source, int period = 10, int smoothPeriod = 10) : this(period, smoothPeriod) { var pubEvent = source.GetType().GetEvent("Pub"); pubEvent?.AddEventHandler(source, new BarSignal(Sub)); @@ -70,9 +68,8 @@ public sealed class Cvi : AbstractBase public override void Init() { base.Init(); - _ema.Init(); - _buffer.Clear(); - _prevEma = 0; + _ema = 0; + _smoothed.Clear(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -80,6 +77,7 @@ public sealed class Cvi : AbstractBase { if (isNew) { + _lastValidValue = Value; _index++; } } @@ -89,20 +87,32 @@ public sealed class Cvi : AbstractBase { ManageState(BarInput.IsNew); - double highLowDiff = BarInput.High - BarInput.Low; - _buffer.Add(highLowDiff, BarInput.IsNew); + // Calculate High-Low difference + double hl = BarInput.High - BarInput.Low; - double ema = _ema.Calc(new TValue(Input.Time, highLowDiff, BarInput.IsNew)).Value; - - double cvi = 0; - if (_index >= _period) + // Calculate EMA of High-Low difference + if (_index == 1) { - double prevEma = _buffer[_buffer.Count - _period]; - cvi = (ema - prevEma) / prevEma * 100; + _ema = hl; + } + else + { + _ema = (_alpha * hl) + ((1 - _alpha) * _ema); } - _prevEma = ema; + // Add smoothed value to buffer + _smoothed.Add(_ema); + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate rate of change + double roc = ((_ema - _smoothed[_period - 1]) / _smoothed[_period - 1]) * 100; + IsHot = _index >= WarmupPeriod; - return cvi; + return roc; } } diff --git a/lib/volatility/Ewma.cs b/lib/volatility/Ewma.cs new file mode 100644 index 00000000..52fb9443 --- /dev/null +++ b/lib/volatility/Ewma.cs @@ -0,0 +1,141 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// EWMA: Exponential Weighted Moving Average Volatility +/// A volatility measure that gives more weight to recent observations, +/// calculated using squared returns and exponential weighting. +/// +/// +/// The EWMA calculation process: +/// 1. Calculate returns: (Close[t] - Close[t-1])/Close[t-1] +/// 2. Square returns +/// 3. Apply exponential weighting to squared returns +/// 4. Take square root and annualize +/// +/// Key characteristics: +/// - More responsive to recent volatility changes +/// - Default decay factor (lambda) is 0.94 +/// - Default period is 20 days +/// - Annualized by default (multiply by sqrt(252)) +/// - Expressed as a percentage +/// +/// Formula: +/// Returns[t] = (Close[t] - Close[t-1])/Close[t-1] +/// EWMA[t] = λ * EWMA[t-1] + (1-λ) * Returns[t]² +/// Volatility = sqrt(EWMA) * sqrt(252) * 100 +/// +/// Where: +/// λ (lambda) = decay factor (typically 0.94) +/// +/// Market Applications: +/// - Risk measurement +/// - Option pricing +/// - Value at Risk (VaR) +/// - Portfolio optimization +/// - Volatility forecasting +/// +/// Sources: +/// RiskMetrics™ Technical Document (1996) +/// https://www.msci.com/documents/10199/5915b101-4206-4ba0-aee2-3449d5c7e95a +/// +/// Note: Returns annualized volatility as a percentage +/// + +[SkipLocalsInit] +public sealed class Ewma : AbstractBase +{ + private readonly int _period; + private readonly double _lambda; + private readonly bool _annualize; + private double _prevClose; + private double _ewma; + private bool _isInitialized; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ewma(int period = 20, double lambda = 0.94, bool annualize = true) + { + _period = period; + _lambda = lambda; + _annualize = annualize; + WarmupPeriod = period + 1; // Need one extra period for returns + Name = $"EWMA({_period},{_lambda})"; + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Ewma(object source, int period = 20, double lambda = 0.94, bool annualize = true) : this(period, lambda, annualize) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _ewma = 0; + _isInitialized = false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate return + double return_ = (BarInput.Close - _prevClose) / _prevClose; + double squaredReturn = return_ * return_; + _prevClose = BarInput.Close; + + // Initialize EWMA if not done + if (!_isInitialized && _index > _period) + { + _ewma = squaredReturn; + _isInitialized = true; + } + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Update EWMA + _ewma = _lambda * _ewma + (1 - _lambda) * squaredReturn; + + // Calculate volatility + double volatility = Math.Sqrt(_ewma); + + // Annualize if requested + if (_annualize) + { + volatility *= Math.Sqrt(252); + } + + // Convert to percentage + volatility *= 100; + + IsHot = _index >= WarmupPeriod; + return volatility; + } +} diff --git a/lib/volatility/Fcb.cs b/lib/volatility/Fcb.cs new file mode 100644 index 00000000..45c61aa9 --- /dev/null +++ b/lib/volatility/Fcb.cs @@ -0,0 +1,159 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// FCB: Fractal Chaos Bands +/// Adaptive price bands based on fractal geometry concepts, +/// identifying potential support and resistance levels. +/// +/// +/// The FCB calculation process: +/// 1. Identify fractal highs and lows over the period +/// 2. Calculate high and low bands using fractal points +/// 3. Smooth bands using exponential moving average +/// +/// Key characteristics: +/// - Adapts to market structure +/// - Default period is 20 days +/// - Default smoothing factor is 0.5 +/// - Returns upper and lower bands +/// - Based on fractal geometry concepts +/// +/// Formula: +/// Fractal High = High[t] where High[t] > High[t±1,2] +/// Fractal Low = Low[t] where Low[t] < Low[t±1,2] +/// Upper Band = EMA(Fractal Highs, smoothing) +/// Lower Band = EMA(Fractal Lows, smoothing) +/// +/// Market Applications: +/// - Support/resistance identification +/// - Trend analysis +/// - Volatility measurement +/// - Breakout detection +/// - Trading range analysis +/// +/// Sources: +/// Bill Williams' Chaos Theory +/// Trading Chaos (2nd Edition) by Bill Williams +/// +/// Note: Returns three values: upper, middle, and lower bands +/// + +[SkipLocalsInit] +public sealed class Fcb : AbstractBase +{ + private readonly double _smoothing; + private readonly CircularBuffer _highs; + private readonly CircularBuffer _lows; + private double _upperBand; + private double _middleBand; + private double _lowerBand; + private double _upperEma; + private double _lowerEma; + private readonly double _alpha; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Fcb(int period = 20, double smoothing = 0.5) + { + _smoothing = smoothing; + _alpha = 2.0 / (period + 1); + WarmupPeriod = period + 4; // Need extra periods for fractal identification + Name = $"FCB({period},{_smoothing})"; + _highs = new CircularBuffer(period); + _lows = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Fcb(object source, int period = 20, double smoothing = 0.5) : this(period, smoothing) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _upperBand = 0; + _middleBand = 0; + _lowerBand = 0; + _upperEma = 0; + _lowerEma = 0; + _highs.Clear(); + _lows.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Add current high/low to buffers + _highs.Add(BarInput.High); + _lows.Add(BarInput.Low); + + // Need enough values for calculation + if (_index <= 4) + { + return 0; + } + + // Check for fractal patterns + bool isFractalHigh = false; + bool isFractalLow = false; + + // Fractal high: current high is higher than 2 bars before and after + isFractalHigh = _highs[2] > _highs[0] && _highs[2] > _highs[1] && + _highs[2] > _highs[3] && _highs[2] > _highs[4]; + + // Fractal low: current low is lower than 2 bars before and after + isFractalLow = _lows[2] < _lows[0] && _lows[2] < _lows[1] && + _lows[2] < _lows[3] && _lows[2] < _lows[4]; + + + // Update EMAs with fractal points + if (isFractalHigh) + { + _upperEma = (_alpha * _highs[2]) + ((1 - _alpha) * _upperEma); + } + if (isFractalLow) + { + _lowerEma = (_alpha * _lows[2]) + ((1 - _alpha) * _lowerEma); + } + + // Apply smoothing to bands + _upperBand = _smoothing * _upperEma + (1 - _smoothing) * BarInput.High; + _lowerBand = _smoothing * _lowerEma + (1 - _smoothing) * BarInput.Low; + _middleBand = (_upperBand + _lowerBand) / 2; + + IsHot = _index >= WarmupPeriod; + return _middleBand; // Return middle band as primary value + } + + /// + /// Gets the upper band value + /// + public double UpperBand => _upperBand; + + /// + /// Gets the middle band value + /// + public double MiddleBand => _middleBand; + + /// + /// Gets the lower band value + /// + public double LowerBand => _lowerBand; +} diff --git a/lib/volatility/Gkv.cs b/lib/volatility/Gkv.cs new file mode 100644 index 00000000..19baebdb --- /dev/null +++ b/lib/volatility/Gkv.cs @@ -0,0 +1,127 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// GKV: Garman-Klass Volatility +/// An efficient estimator of volatility that uses open, high, low, +/// and close prices to capture intraday price movements. +/// +/// +/// The GKV calculation process: +/// 1. Calculate components using OHLC prices +/// 2. Combine components using optimal weights +/// 3. Take rolling average over period +/// 4. Annualize and convert to percentage +/// +/// Key characteristics: +/// - More efficient than close-to-close volatility +/// - Uses full OHLC price information +/// - Default period is 20 days +/// - Annualized by default +/// - Expressed as a percentage +/// +/// Formula: +/// u = ln(High/Low)²/2 +/// c = ln(Close/Open)² +/// GKV = sqrt(sum((0.5*u - (2*ln(2)-1)*c) / period) * 252) * 100 +/// +/// Market Applications: +/// - Volatility estimation +/// - Risk measurement +/// - Option pricing +/// - Trading strategy development +/// - Market analysis +/// +/// Sources: +/// Garman and Klass (1980) +/// Journal of Business 53(1): 67-78 +/// +/// Note: Returns annualized volatility as a percentage +/// + +[SkipLocalsInit] +public sealed class Gkv : AbstractBase +{ + private readonly int _period; + private readonly bool _annualize; + private readonly CircularBuffer _components; + private readonly double _ln2; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Gkv(int period = 20, bool annualize = true) + { + _period = period; + _annualize = annualize; + WarmupPeriod = period; + Name = $"GKV({_period})"; + _components = new CircularBuffer(period); + _ln2 = Math.Log(2); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Gkv(object source, int period = 20, bool annualize = true) : this(period, annualize) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _components.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Calculate components + double u = Math.Log(BarInput.High / BarInput.Low); + u = u * u / 2; + + double c = Math.Log(BarInput.Close / BarInput.Open); + c = c * c; + + // Combine components with optimal weights + double component = 0.5 * u - (2 * _ln2 - 1) * c; + _components.Add(component); + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate average component + double avgComponent = _components.Average(); + + // Calculate volatility + double volatility = Math.Sqrt(avgComponent); + + // Annualize if requested + if (_annualize) + { + volatility *= Math.Sqrt(252); + } + + // Convert to percentage + volatility *= 100; + + IsHot = _index >= WarmupPeriod; + return volatility; + } +} diff --git a/lib/volatility/Hlv.cs b/lib/volatility/Hlv.cs new file mode 100644 index 00000000..63a0b3ea --- /dev/null +++ b/lib/volatility/Hlv.cs @@ -0,0 +1,130 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// HLV: High-Low Volatility +/// A volatility measure based on the high-low range relative +/// to the previous close, capturing intraday price movements. +/// +/// +/// The HLV calculation process: +/// 1. Calculate normalized high-low range +/// 2. Take rolling average over period +/// 3. Convert to annualized volatility +/// +/// Key characteristics: +/// - Captures intraday price movements +/// - Uses high, low, and previous close +/// - Default period is 20 days +/// - Annualized by default +/// - Expressed as a percentage +/// +/// Formula: +/// Range = (High - Low) / PrevClose +/// HLV = sqrt(sum(Range² / period) * 252) * 100 +/// +/// Market Applications: +/// - Volatility measurement +/// - Risk assessment +/// - Trading range analysis +/// - Market regime identification +/// - Position sizing +/// +/// Sources: +/// Parkinson (1980) modified +/// The Extreme Value Method for Estimating the Variance of the Rate of Return +/// Journal of Business 53(1): 61-65 +/// +/// Note: Returns annualized volatility as a percentage +/// + +[SkipLocalsInit] +public sealed class Hlv : AbstractBase +{ + private readonly int _period; + private readonly bool _annualize; + private readonly CircularBuffer _ranges; + private double _prevClose; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hlv(int period = 20, bool annualize = true) + { + _period = period; + _annualize = annualize; + WarmupPeriod = period + 1; // Need one extra period for previous close + Name = $"HLV({_period})"; + _ranges = new CircularBuffer(period); + Init(); + } + + /// The data source object that publishes updates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Hlv(object source, int period = 20, bool annualize = true) : this(period, annualize) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _prevClose = 0; + _ranges.Clear(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _lastValidValue = Value; + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + // Skip first period to establish previous close + if (_index == 1) + { + _prevClose = BarInput.Close; + return 0; + } + + // Calculate normalized range + double range = (BarInput.High - BarInput.Low) / _prevClose; + double squaredRange = range * range; + _ranges.Add(squaredRange); + + // Store current close for next calculation + _prevClose = BarInput.Close; + + // Need enough values for calculation + if (_index <= _period) + { + return 0; + } + + // Calculate average squared range + double avgSquaredRange = _ranges.Average(); + + // Calculate volatility + double volatility = Math.Sqrt(avgSquaredRange); + + // Annualize if requested + if (_annualize) + { + volatility *= Math.Sqrt(252); + } + + // Convert to percentage + volatility *= 100; + + IsHot = _index >= WarmupPeriod; + return volatility; + } +} diff --git a/lib/volatility/_list.md b/lib/volatility/_list.md index 49854141..9e0f4094 100644 --- a/lib/volatility/_list.md +++ b/lib/volatility/_list.md @@ -1,21 +1,21 @@ # Volatility indicators -Done: 15, Todo: 20 +Done: 24, Todo: 11 ✔️ ADR - Average Daily Range ✔️ AP - Andrew's Pitchfork ✔️ ATR - Average True Range ✔️ ATRP - Average True Range Percent ✔️ ATRS - ATR Trailing Stop -*BB - Bollinger Bands® (Upper, Middle, Lower) -CCV - Close-to-Close Volatility -CE - Chandelier Exit -CV - Conditional Volatility (ARCH/GARCH) -CVI - Chaikin's Volatility +✔️ BBAND - Bollinger Bands® (Upper, Middle, Lower) +✔️ CCV - Close-to-Close Volatility +✔️ CE - Chandelier Exit +✔️ CV - Conditional Volatility (ARCH/GARCH) +✔️ CVI - Chaikin's Volatility *DC - Donchian Channels (Upper, Middle, Lower) -EWMA - Exponential Weighted Moving Average Volatility -FCB - Fractal Chaos Bands -GKV - Garman-Klass Volatility -HLV - High-Low Volatility +✔️ EWMA - Exponential Weighted Moving Average Volatility +✔️ FCB - Fractal Chaos Bands +✔️ GKV - Garman-Klass Volatility +✔️ HLV - High-Low Volatility ✔️ HV - Historical Volatility *ICH - Ichimoku Cloud (Conversion, Base, Leading Span A, Leading Span B, Lagging Span) ✔️ JVOLTY - Jurik Volatility diff --git a/quantower/Averages/_Averages.csproj b/quantower/Averages/_Averages.csproj index 8d946a96..ea03d5a8 100644 --- a/quantower/Averages/_Averages.csproj +++ b/quantower/Averages/_Averages.csproj @@ -2,13 +2,10 @@ Averages Indicator - 0.0.0.0 bin\$(Configuration)\ - true - true - true false + @@ -16,7 +13,6 @@ - ..\..\.github\TradingPlatform.BusinessLayer.dll @@ -32,4 +28,4 @@ DestinationFolder="$(QuantowerRoot)\Settings\Scripts\Indicators\QuanTAlib\Averages" /> - + \ No newline at end of file diff --git a/quantower/Momentum/_Momentum.csproj b/quantower/Momentum/_Momentum.csproj index 00cb1a71..29adbe09 100644 --- a/quantower/Momentum/_Momentum.csproj +++ b/quantower/Momentum/_Momentum.csproj @@ -2,13 +2,10 @@ Momentum Indicator - 0.0.0.0 bin\$(Configuration)\ - true - true - true false + @@ -30,4 +27,4 @@ - + \ No newline at end of file diff --git a/quantower/Oscillators/_Oscillators.csproj b/quantower/Oscillators/_Oscillators.csproj index ed3a39fe..11c13ec8 100644 --- a/quantower/Oscillators/_Oscillators.csproj +++ b/quantower/Oscillators/_Oscillators.csproj @@ -2,13 +2,10 @@ Oscillators Indicator - 0.0.0.0 bin\$(Configuration)\ - true - true - true false + @@ -30,4 +27,4 @@ - + \ No newline at end of file diff --git a/quantower/Statistics/_Statistics.csproj b/quantower/Statistics/_Statistics.csproj index 4904fae6..f67a5d55 100644 --- a/quantower/Statistics/_Statistics.csproj +++ b/quantower/Statistics/_Statistics.csproj @@ -2,13 +2,10 @@ Statistics Indicator - 0.0.0.0 bin\$(Configuration)\ - true - true - true false + diff --git a/quantower/Volatility/_Volatility.csproj b/quantower/Volatility/_Volatility.csproj index 0a8f58d8..2612c385 100644 --- a/quantower/Volatility/_Volatility.csproj +++ b/quantower/Volatility/_Volatility.csproj @@ -2,13 +2,10 @@ Volatility Indicator - 0.0.0.0 bin\$(Configuration)\ - true - true - true false + @@ -16,7 +13,6 @@ - ..\..\.github\TradingPlatform.BusinessLayer.dll @@ -31,4 +27,4 @@ - + \ No newline at end of file diff --git a/quantower/Volume/_Volume.csproj b/quantower/Volume/_Volume.csproj index 13266f2a..db6a0d69 100644 --- a/quantower/Volume/_Volume.csproj +++ b/quantower/Volume/_Volume.csproj @@ -2,13 +2,10 @@ Volume Indicator - 0.0.0.0 bin\$(Configuration)\ - true - true - true false + @@ -30,4 +27,4 @@ - + \ No newline at end of file