diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5c462ec8..dabdd4cc 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -2,7 +2,7 @@ name: CI/CD on: push: - branches: [ main, develop ] + branches: [ main, dev ] pull_request: workflow_dispatch: @@ -76,7 +76,7 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} release_branches: main - pre_release_branches: develop + pre_release_branches: dev dry_run: true default_bump: ${{ steps.check_tags.outputs.has_tags == 'false' && 'major' || 'false' }} custom_release_rules: | diff --git a/.gitignore b/.gitignore index fbf58e72..df916e3e 100644 --- a/.gitignore +++ b/.gitignore @@ -287,3 +287,12 @@ __pycache__/ *.odx.cs *.xsd.cs .DS_Store + +# Agent tooling scratch directories and generated state +.auto-claude/ +.auto-claude-security.json +.auto-claude-status +.claude_settings.json +.worktrees/ +.security-key +logs/security/ diff --git a/Directory.Packages.props b/Directory.Packages.props index 79f2303e..124576de 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -29,5 +29,9 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index deabc41d..43614355 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,16 @@ A modern and robust C# Technical Analysis library based on the original open-sou * [πŸ“Œ Features](#-features) * [Roadmap (next features)](#roadmap-next-features) * [πŸ“„ Documentation](#-documentation) + * [πŸ“– Guides](#-guides) * [πŸ“₯ Installation](#-installation) * [πŸ“‹ Prerequisites](#-prerequisites) * [πŸš€ We use the latest C# features](#-we-use-the-latest-c-features) * [πŸ“¦ NuGet Packages](#-nuget-packages) * [πŸ§ͺ Tests Specifications](#-tests-specifications) * [πŸ’Ύ Installation](#-installation-1) + * [πŸ§‘β€πŸ’» Usage](#-usage) + * [🧩 Samples](#-samples) + * [⚑ Benchmarks](#-benchmarks) * [πŸ“Š Code Quality](#-code-quality) * [❓ Issues and Feature Requests](#-issues-and-feature-requests) * [🀝 Contributing](#-contributing) @@ -59,7 +63,12 @@ The primary objective of TaLibStandard is to provide a comprehensive, feature-ri ## 🏁 Getting started -To get started with TaLibStandard, you can clone the repository and explore the examples provided in the `examples` directory. You can also refer to the list of [available functions](./docs/functions.md) in the documentation for a comprehensive overview of the library's capabilities. +To get started with TaLibStandard, read the [getting started guide](./docs/guides/getting-started.md) β€” +it covers installation, your first indicator, and the output-alignment rule that everything else depends +on. Then clone the repository and explore the runnable projects in the [`samples`](./samples) directory +(see [Samples](#-samples)). For a comprehensive overview of the library's capabilities, refer to the +[indicator catalog](./docs/indicators/README.md) or the flat list of +[available functions](./docs/functions.md). ## πŸ“Œ Features @@ -70,12 +79,12 @@ To get started with TaLibStandard, you can clone the repository and explore the ### Roadmap (next features) * [ ] Comprehensive API documentation that is easy to understand -* [ ] High-Level API for common use cases +* [x] High-Level API for common use cases β€” see the [fluent API guide](./docs/guides/fluent-api.md) * [ ] Support for more data types * [ ] Support for more functions * [ ] More tests -* [ ] More examples -* [ ] Add a Benchmark project +* [x] More examples β€” see [Samples](#-samples) +* [x] Add a Benchmark project β€” see [Benchmarks](#-benchmarks) * [ ] Create a gRPC server to expose the library as a service ## πŸ“„ Documentation @@ -87,6 +96,21 @@ discuss it. > **Note:** The documentation is generated using [Doraku/DefaultDocumentation]() tool. It is generated automatically when the project is built. +## πŸ“– Guides + +Hand-written guides live in [`docs/guides`](./docs/guides), and every public entry point is catalogued in +[`docs/indicators`](./docs/indicators/README.md). + +| Guide | What it covers | +|-------|----------------| +| [🏁 Getting started](./docs/guides/getting-started.md) | Installation, your first indicator, and the three things that trip everyone up: `RetCode`, `BegIdx`/`NBElement` output alignment, and the `double` / `float` / `decimal` story. **Start here.** | +| [✨ Fluent API](./docs/guides/fluent-api.md) | `PriceSeries` in, bar-indexed `IndicatorSeries` out β€” the layer that does the `BegIdx`/`NBElement` arithmetic for you, with `null` for a bar that has not warmed up. Warm-up semantics, crossings, `AsOf`, the nine shipped indicators and the `Align` escape hatch to the rest. | +| [πŸ“‹ Indicator catalog](./docs/indicators/README.md) | Every `TAMath` and `TACandle` entry point, grouped by category, with signatures, defaults, outputs and links to the generated API pages. | +| [πŸ“‘ Real-time streaming](./docs/guides/real-time-streaming.md) | Ticks β†’ bars β†’ indicators over SignalR and raw WebSocket: architecture, message contracts, warm-up semantics and production notes. | +| [πŸ“‰ Backtesting](./docs/guides/backtesting.md) | The engine model, the structurally enforced no-look-ahead guarantee, the cost model, every metric with its formula, and how to write your own strategy. | +| [πŸ“ˆ TradingView integration](./docs/guides/tradingview-integration.md) | Pine Script `ta.*` β†’ `TAMath` mapping, parity caveats, UDF datafeed and Lightweight Charts wiring, alert-webhook security. | +| [⚑ Benchmarks](./docs/guides/benchmarks.md) | What the benchmark suite measures, how to run it, how to read BenchmarkDotNet output, and the measured results. | + ## πŸ“₯ Installation ### πŸ“‹ Prerequisites @@ -130,11 +154,31 @@ dotnet add package Atypical.TechnicalAnalysis.Functions ## πŸ§‘β€πŸ’» Usage -TaLibStandard exposes two APIs on the same indicator: a low-level `TAFunc` API that mirrors the -original TA-Lib C signature (`ref`/`in` parameters, pre-allocated output arrays), and a higher-level -`TAMath` API that wraps it and returns a strongly-typed result record. +TaLibStandard exposes three APIs over the same indicators: a **fluent** API (`PriceSeries` / +`IndicatorSeries`) that hands you values addressed by bar index, a **`TAMath`** API that returns a +strongly-typed result record carrying TA-Lib's raw output array and its alignment metadata, and a +low-level **`TAFunc`** API that mirrors the original TA-Lib C signature (`ref`/`in` parameters, +pre-allocated output arrays). -### High-level API (`TAMath`) +### Fluent API (`PriceSeries` β†’ `IndicatorSeries`) + +```csharp +using TechnicalAnalysis.Functions; + +PriceSeries prices = PriceSeries.FromHlc(highs, lows, closes); + +double? rsi = prices.Rsi(14).Latest; // null until the indicator has warmed up +double? atr = prices.Atr(14).Latest; + +IndicatorSeries fast = prices.Sma(5); +IndicatorSeries slow = prices.Sma(20); +bool goldenCross = fast.CrossedAbove(slow, bar: prices.BarCount - 1); +``` + +Every index is a **bar** index, and a bar the indicator has not reached yet is `null` β€” never `0.0`. +See the [fluent API guide](./docs/guides/fluent-api.md). + +### `TAMath` β€” the raw result record ```csharp using TechnicalAnalysis.Functions; @@ -144,9 +188,11 @@ double[] closingPrices = [.. /* your OHLCV data */]; // RsiResult exposes RetCode, BegIdx, NBElement and the Real[] output array RsiResult rsi = TAMath.Rsi(0, closingPrices.Length - 1, closingPrices, timePeriod: 14); -if (rsi.RetCode == RetCode.Success) +if (rsi.RetCode == RetCode.Success && rsi.NBElement > 0) { - double latestRsi = rsi.Real[^1]; // most recent RSI value + // The newest value is at array index NBElement - 1, and it describes + // bar BegIdx + NBElement - 1. Those are two different numbers. + double latestRsi = rsi.Real[rsi.NBElement - 1]; } ``` @@ -187,6 +233,54 @@ inputs. See the [full function list](./docs/functions.md) for every available in candlestick pattern, and the [Demo.BlazorWasm](./Demo.BlazorWasm) project for a working end-to-end example that charts these indicators. +> **One rule to internalise before anything else.** `TAMath` fills its output array from index `0`, not +> from the input index it corresponds to. Output element `k` describes **input index `BegIdx + k`**, for +> `k` in `[0, NBElement)`; everything from `NBElement` onwards is a meaningless zero. Getting this wrong +> shifts every signal in time, silently. The [getting started guide](./docs/guides/getting-started.md) +> works through it with a hand-checkable example, and the +> [fluent API](./docs/guides/fluent-api.md) does the arithmetic for you in one tested place. + +## 🧩 Samples + +Runnable projects, all completely offline β€” no market data provider, no API key, no network calls. + +| Sample | Run it | Guide | +|--------|--------|-------| +| [**Real-time streaming**](./samples/TechnicalAnalysis.Samples.RealTime)
ASP.NET Core server: synthetic tick feed β†’ OHLCV bars β†’ seven indicators (eleven series) per closed bar, published over a SignalR hub *and* a raw WebSocket, plus a zero-dependency browser dashboard. | `dotnet run --project samples/TechnicalAnalysis.Samples.RealTime -c Release`
then open | [πŸ“‘ Real-time streaming](./docs/guides/real-time-streaming.md) | +| [**Real-time console client**](./samples/TechnicalAnalysis.Samples.RealTime.Client)
SignalR client for the server above; exercises both the group-push and the server-streaming paths. | `dotnet run --project samples/TechnicalAnalysis.Samples.RealTime.Client -c Release -- --symbol GLOBEX` | [πŸ“‘ Real-time streaming](./docs/guides/real-time-streaming.md) | +| [**Backtesting**](./samples/TechnicalAnalysis.Samples.Backtesting)
Bar-by-bar engine with a structurally enforced no-look-ahead guarantee, a commission/slippage cost model, a full metrics suite and five strategies compared side by side. | `dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -c Release` | [πŸ“‰ Backtesting](./docs/guides/backtesting.md) | +| [**Blazor WebAssembly demo**](./Demo.BlazorWasm)
Interactive browser demo charting the indicators. | `dotnet run --project Demo.BlazorWasm` | β€” | + +## ⚑ Benchmarks + +[`benchmarks/TechnicalAnalysis.Benchmarks`](./benchmarks/TechnicalAnalysis.Benchmarks) is a +BenchmarkDotNet suite of **119 benchmarks** over deterministic synthetic market data at three series +lengths (1 000 / 10 000 / 100 000), all with `[MemoryDiagnoser]`. Every indicator in the overlap, +momentum and volatility/volume suites is measured **twice** β€” once through the allocation-free `TAFunc` +API and once through the ergonomic `TAMath` API β€” so the cost of convenience is a number rather than a +guess. Candlestick patterns are measured on `double`, `float` **and** `decimal` to price the +generic-math design. + +```shell +# see what is there, without running anything +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --list flat + +# prove every benchmark computes something valid (fast; not a measurement) +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --selfcheck + +# one suite +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --anyCategories Momentum +``` + +An optional sixth suite compares the managed port head to head against the original TA-Lib C library +through P/Invoke, with an equivalence assertion that runs *before* anything is timed. It is enabled +automatically when the native library is found and silently skipped when it is not, so the suite has no +native dependency. + +See the [benchmarks guide](./docs/guides/benchmarks.md) for the full switch reference, the native +install instructions per platform, how to read every output column, the measured results and the +methodology caveats. + ## πŸ“Š Code Quality We strive for the highest code quality in TaLibStandard, leveraging Codacyβ€”an automated code analysis/quality tool. Codacy provides static analysis, cyclomatic complexity measures, duplication identification, and code unit test coverage changes for every commit and pull request. diff --git a/TaLibStandard.sln b/TaLibStandard.sln index 00b78621..374e7497 100644 --- a/TaLibStandard.sln +++ b/TaLibStandard.sln @@ -37,77 +37,178 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.BlazorWasm", "Demo.Bla EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{A5E5C4A5-0E5C-4F68-B5E5-E5C5F5E5C5F5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{66320409-64EC-F7C5-3DEF-65E7510DAAD1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnicalAnalysis.Benchmarks", "benchmarks\TechnicalAnalysis.Benchmarks\TechnicalAnalysis.Benchmarks.csproj", "{5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnicalAnalysis.Samples.Backtesting", "samples\TechnicalAnalysis.Samples.Backtesting\TechnicalAnalysis.Samples.Backtesting.csproj", "{4648FCAC-48E5-40BC-A6D2-01DA9E671533}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnicalAnalysis.Samples.RealTime", "samples\TechnicalAnalysis.Samples.RealTime\TechnicalAnalysis.Samples.RealTime.csproj", "{B95318E3-0681-4D0B-A99E-EFC4592A2916}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnicalAnalysis.Samples.RealTime.Client", "samples\TechnicalAnalysis.Samples.RealTime.Client\TechnicalAnalysis.Samples.RealTime.Client.csproj", "{81CA190F-CC16-4A06-8B21-410039E9F04E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TechnicalAnalysis.Samples.Backtesting.UnitTests", "tests\TechnicalAnalysis.Samples.Backtesting.UnitTests\TechnicalAnalysis.Samples.Backtesting.UnitTests.csproj", "{590EA42C-9B12-472B-9D1F-1B7ADA156109}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x86 = Debug|x86 + Debug|x64 = Debug|x64 Release|Any CPU = Release|Any CPU Release|x86 = Release|x86 + Release|x64 = Release|x64 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {AE28059B-2495-4229-A90E-5CE9D70334D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Debug|Any CPU.Build.0 = Debug|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Debug|x86.ActiveCfg = Debug|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Debug|x86.Build.0 = Debug|Any CPU + {AE28059B-2495-4229-A90E-5CE9D70334D3}.Debug|x64.ActiveCfg = Debug|Any CPU + {AE28059B-2495-4229-A90E-5CE9D70334D3}.Debug|x64.Build.0 = Debug|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Release|Any CPU.ActiveCfg = Release|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Release|Any CPU.Build.0 = Release|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Release|x86.ActiveCfg = Release|Any CPU {AE28059B-2495-4229-A90E-5CE9D70334D3}.Release|x86.Build.0 = Release|Any CPU + {AE28059B-2495-4229-A90E-5CE9D70334D3}.Release|x64.ActiveCfg = Release|Any CPU + {AE28059B-2495-4229-A90E-5CE9D70334D3}.Release|x64.Build.0 = Release|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Debug|Any CPU.Build.0 = Debug|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Debug|x86.ActiveCfg = Debug|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Debug|x86.Build.0 = Debug|Any CPU + {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Debug|x64.ActiveCfg = Debug|Any CPU + {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Debug|x64.Build.0 = Debug|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Release|Any CPU.ActiveCfg = Release|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Release|Any CPU.Build.0 = Release|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Release|x86.ActiveCfg = Release|Any CPU {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Release|x86.Build.0 = Release|Any CPU + {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Release|x64.ActiveCfg = Release|Any CPU + {552FF382-7E2F-4566-8DF8-2BA0F752DA36}.Release|x64.Build.0 = Release|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Debug|Any CPU.Build.0 = Debug|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Debug|x86.ActiveCfg = Debug|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Debug|x86.Build.0 = Debug|Any CPU + {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Debug|x64.ActiveCfg = Debug|Any CPU + {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Debug|x64.Build.0 = Debug|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Release|Any CPU.ActiveCfg = Release|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Release|Any CPU.Build.0 = Release|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Release|x86.ActiveCfg = Release|Any CPU {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Release|x86.Build.0 = Release|Any CPU + {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Release|x64.ActiveCfg = Release|Any CPU + {70E407F2-1332-408C-B11F-A64E2A3FFF99}.Release|x64.Build.0 = Release|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Debug|Any CPU.Build.0 = Debug|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Debug|x86.ActiveCfg = Debug|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Debug|x86.Build.0 = Debug|Any CPU + {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Debug|x64.ActiveCfg = Debug|Any CPU + {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Debug|x64.Build.0 = Debug|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Release|Any CPU.ActiveCfg = Release|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Release|Any CPU.Build.0 = Release|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Release|x86.ActiveCfg = Release|Any CPU {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Release|x86.Build.0 = Release|Any CPU + {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Release|x64.ActiveCfg = Release|Any CPU + {9F038FE8-EB6F-45D2-A6B4-BBFED44CFBB0}.Release|x64.Build.0 = Release|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Debug|x86.ActiveCfg = Debug|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Debug|x86.Build.0 = Debug|Any CPU + {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Debug|x64.ActiveCfg = Debug|Any CPU + {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Debug|x64.Build.0 = Debug|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Release|Any CPU.Build.0 = Release|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Release|x86.ActiveCfg = Release|Any CPU {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Release|x86.Build.0 = Release|Any CPU + {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Release|x64.ActiveCfg = Release|Any CPU + {EDB2827E-1163-49E6-9120-FED9E1ABDB62}.Release|x64.Build.0 = Release|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Debug|x86.ActiveCfg = Debug|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Debug|x86.Build.0 = Debug|Any CPU + {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Debug|x64.ActiveCfg = Debug|Any CPU + {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Debug|x64.Build.0 = Debug|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Release|Any CPU.Build.0 = Release|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Release|x86.ActiveCfg = Release|Any CPU {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Release|x86.Build.0 = Release|Any CPU + {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Release|x64.ActiveCfg = Release|Any CPU + {B1B33276-DEFE-4183-8DEC-D068356BD02A}.Release|x64.Build.0 = Release|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Debug|Any CPU.Build.0 = Debug|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Debug|x86.ActiveCfg = Debug|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Debug|x86.Build.0 = Debug|Any CPU + {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Debug|x64.ActiveCfg = Debug|Any CPU + {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Debug|x64.Build.0 = Debug|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Release|Any CPU.ActiveCfg = Release|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Release|Any CPU.Build.0 = Release|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Release|x86.ActiveCfg = Release|Any CPU {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Release|x86.Build.0 = Release|Any CPU + {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Release|x64.ActiveCfg = Release|Any CPU + {F07C261D-D025-4E12-B260-DDD09E83DC0C}.Release|x64.Build.0 = Release|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Debug|x86.ActiveCfg = Debug|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Debug|x86.Build.0 = Debug|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Debug|x64.ActiveCfg = Debug|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Debug|x64.Build.0 = Debug|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Release|Any CPU.Build.0 = Release|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Release|x86.ActiveCfg = Release|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Release|x86.Build.0 = Release|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Release|x64.ActiveCfg = Release|Any CPU + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80}.Release|x64.Build.0 = Release|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Debug|x86.ActiveCfg = Debug|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Debug|x86.Build.0 = Debug|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Debug|x64.ActiveCfg = Debug|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Debug|x64.Build.0 = Debug|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Release|Any CPU.Build.0 = Release|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Release|x86.ActiveCfg = Release|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Release|x86.Build.0 = Release|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Release|x64.ActiveCfg = Release|Any CPU + {4648FCAC-48E5-40BC-A6D2-01DA9E671533}.Release|x64.Build.0 = Release|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Debug|x86.ActiveCfg = Debug|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Debug|x86.Build.0 = Debug|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Debug|x64.ActiveCfg = Debug|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Debug|x64.Build.0 = Debug|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Release|Any CPU.Build.0 = Release|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Release|x86.ActiveCfg = Release|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Release|x86.Build.0 = Release|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Release|x64.ActiveCfg = Release|Any CPU + {B95318E3-0681-4D0B-A99E-EFC4592A2916}.Release|x64.Build.0 = Release|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Debug|x86.ActiveCfg = Debug|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Debug|x86.Build.0 = Debug|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Debug|x64.ActiveCfg = Debug|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Debug|x64.Build.0 = Debug|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Release|Any CPU.Build.0 = Release|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Release|x86.ActiveCfg = Release|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Release|x86.Build.0 = Release|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Release|x64.ActiveCfg = Release|Any CPU + {81CA190F-CC16-4A06-8B21-410039E9F04E}.Release|x64.Build.0 = Release|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Debug|Any CPU.Build.0 = Debug|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Debug|x86.ActiveCfg = Debug|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Debug|x86.Build.0 = Debug|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Debug|x64.ActiveCfg = Debug|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Debug|x64.Build.0 = Debug|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Release|Any CPU.ActiveCfg = Release|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Release|Any CPU.Build.0 = Release|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Release|x86.ActiveCfg = Release|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Release|x86.Build.0 = Release|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Release|x64.ActiveCfg = Release|Any CPU + {590EA42C-9B12-472B-9D1F-1B7ADA156109}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {1122291B-5129-4C67-94CF-1B89AB8C804A} - EndGlobalSection GlobalSection(NestedProjects) = preSolution {AE28059B-2495-4229-A90E-5CE9D70334D3} = {5AEBD15B-4E7E-47F2-B8D9-FEA28EBE87EA} {552FF382-7E2F-4566-8DF8-2BA0F752DA36} = {5AEBD15B-4E7E-47F2-B8D9-FEA28EBE87EA} @@ -116,5 +217,13 @@ Global {EDB2827E-1163-49E6-9120-FED9E1ABDB62} = {CC261FDF-BD3D-46D8-9F87-326E86BDEEF2} {B1B33276-DEFE-4183-8DEC-D068356BD02A} = {CC261FDF-BD3D-46D8-9F87-326E86BDEEF2} {F07C261D-D025-4E12-B260-DDD09E83DC0C} = {A5E5C4A5-0E5C-4F68-B5E5-E5C5F5E5C5F5} + {5471EED6-09D0-42C5-9D93-0B1E7B2DDB80} = {66320409-64EC-F7C5-3DEF-65E7510DAAD1} + {4648FCAC-48E5-40BC-A6D2-01DA9E671533} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {B95318E3-0681-4D0B-A99E-EFC4592A2916} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {81CA190F-CC16-4A06-8B21-410039E9F04E} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + {590EA42C-9B12-472B-9D1F-1B7ADA156109} = {CC261FDF-BD3D-46D8-9F87-326E86BDEEF2} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1122291B-5129-4C67-94CF-1B89AB8C804A} EndGlobalSection EndGlobal diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/BenchmarkCategories.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/BenchmarkCategories.cs new file mode 100644 index 00000000..caa32d5b --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/BenchmarkCategories.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// The category names used by [BenchmarkCategory], so that --anyCategories and +/// --allCategories filters can be written without guessing at spelling. +/// +public static class BenchmarkCategories +{ + /// + /// Moving averages, envelopes and other overlap studies. + /// + public const string OverlapStudies = "OverlapStudies"; + + /// + /// Momentum oscillators. + /// + public const string Momentum = "Momentum"; + + /// + /// Volatility and volume indicators plus the statistic functions. + /// + public const string VolatilityVolume = "VolatilityVolume"; + + /// + /// Candlestick pattern recognisers. + /// + public const string CandlePatterns = "CandlePatterns"; + + /// + /// Numeric precision comparisons. + /// + public const string Precision = "Precision"; + + /// + /// Managed versus native TA-Lib C head-to-head comparisons. + /// + public const string NativeComparison = "NativeComparison"; + + /// + /// The low level, allocation-free TAFunc API with caller-supplied output buffers. + /// + public const string TaFunc = "TAFunc"; + + /// + /// The ergonomic TAMath API that allocates its output arrays and a result record per call. + /// + public const string TaMath = "TAMath"; + + /// + /// Benchmarks operating on inputs. + /// + public const string DoublePrecision = "double"; + + /// + /// Benchmarks operating on inputs. + /// + public const string SinglePrecision = "float"; + + /// + /// Benchmarks operating on inputs. + /// + public const string DecimalPrecision = "decimal"; + + /// + /// The managed TaLibStandard implementation. + /// + public const string Managed = "Managed"; + + /// + /// The original TA-Lib C implementation reached through P/Invoke. + /// + public const string Native = "Native"; +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/CandlePatternBenchmarks.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/CandlePatternBenchmarks.cs new file mode 100644 index 00000000..0c50d56c --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/CandlePatternBenchmarks.cs @@ -0,0 +1,708 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Candles; +using TechnicalAnalysis.Common; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// A representative dozen candlestick pattern recognisers, each exercised over , +/// and inputs. +/// +/// +/// +/// TACandle is generic over T : IFloatingPoint<T>, so the JIT produces a dedicated, fully +/// devirtualised body for each value type. The three variants of every benchmark expose the real cost of that +/// generic-math design: double and float compile down to hardware floating point, whereas +/// decimal falls back to the software 128-bit decimal implementation, which is typically one to two orders +/// of magnitude slower and is the reason a decimal-based pipeline should be a deliberate choice. +/// +/// +/// There is no allocation-free path here: TACandle always allocates the int[] output and a +/// record. The memory columns therefore measure the ergonomic API only. +/// +/// +/// There is deliberately no Ratio column here. A single Baseline = true would +/// ratio every row against one method β€” the default logical group is (Job, Params), not (indicator) β€” so a +/// float row would appear to be compared against its own double counterpart when it was in fact +/// compared against Sma_Double. Compare the Mean and Allocated columns of the two rows +/// of the same indicator instead. +/// +/// +[MemoryDiagnoser] +[CategoriesColumn] +[BenchmarkCategory(BenchmarkCategories.CandlePatterns)] +public class CandlePatternBenchmarks : MarketDataBenchmarkBase +{ + private const double PenetrationDouble = 0.3; + private const float PenetrationSingle = 0.3f; + private const decimal PenetrationDecimal = 0.3m; + + /// + /// Generates the market data. + /// + [GlobalSetup] + public void Setup() + { + InitializeMarketData(); + } + + /// + /// Doji over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult Doji_Double() + { + return TACandle.CdlDoji(StartIdx, EndIdx, Series.Doubles.Open, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close); + } + + /// + /// Doji over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult Doji_Float() + { + return TACandle.CdlDoji(StartIdx, EndIdx, Series.Singles.Open, Series.Singles.High, Series.Singles.Low, Series.Singles.Close); + } + + /// + /// Doji over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult Doji_Decimal() + { + return TACandle.CdlDoji( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Engulfing pattern over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult Engulfing_Double() + { + return TACandle.CdlEngulfing( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Engulfing pattern over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult Engulfing_Float() + { + return TACandle.CdlEngulfing( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Engulfing pattern over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult Engulfing_Decimal() + { + return TACandle.CdlEngulfing( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Hammer over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult Hammer_Double() + { + return TACandle.CdlHammer( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Hammer over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult Hammer_Float() + { + return TACandle.CdlHammer( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Hammer over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult Hammer_Decimal() + { + return TACandle.CdlHammer( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Hanging man over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult HangingMan_Double() + { + return TACandle.CdlHangingMan( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Hanging man over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult HangingMan_Float() + { + return TACandle.CdlHangingMan( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Hanging man over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult HangingMan_Decimal() + { + return TACandle.CdlHangingMan( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Harami over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult Harami_Double() + { + return TACandle.CdlHarami( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Harami over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult Harami_Float() + { + return TACandle.CdlHarami( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Harami over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult Harami_Decimal() + { + return TACandle.CdlHarami( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Marubozu over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult Marubozu_Double() + { + return TACandle.CdlMarubozu( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Marubozu over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult Marubozu_Float() + { + return TACandle.CdlMarubozu( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Marubozu over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult Marubozu_Decimal() + { + return TACandle.CdlMarubozu( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Spinning top over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult SpinningTop_Double() + { + return TACandle.CdlSpinningTop( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Spinning top over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult SpinningTop_Float() + { + return TACandle.CdlSpinningTop( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Spinning top over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult SpinningTop_Decimal() + { + return TACandle.CdlSpinningTop( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Shooting star over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult ShootingStar_Double() + { + return TACandle.CdlShootingStar( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Shooting star over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult ShootingStar_Float() + { + return TACandle.CdlShootingStar( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Shooting star over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult ShootingStar_Decimal() + { + return TACandle.CdlShootingStar( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Three white soldiers over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult ThreeWhiteSoldiers_Double() + { + return TACandle.Cdl3WhiteSoldiers( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Three white soldiers over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult ThreeWhiteSoldiers_Float() + { + return TACandle.Cdl3WhiteSoldiers( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Three white soldiers over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult ThreeWhiteSoldiers_Decimal() + { + return TACandle.Cdl3WhiteSoldiers( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Three black crows over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult ThreeBlackCrows_Double() + { + return TACandle.Cdl3BlackCrows( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Three black crows over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult ThreeBlackCrows_Float() + { + return TACandle.Cdl3BlackCrows( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Three black crows over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult ThreeBlackCrows_Decimal() + { + return TACandle.Cdl3BlackCrows( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Piercing pattern over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult Piercing_Double() + { + return TACandle.CdlPiercing( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// Piercing pattern over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult Piercing_Float() + { + return TACandle.CdlPiercing( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// Piercing pattern over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult Piercing_Decimal() + { + return TACandle.CdlPiercing( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// High wave candle over double inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult HighWave_Double() + { + return TACandle.CdlHighWave( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close); + } + + /// + /// High wave candle over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult HighWave_Float() + { + return TACandle.CdlHighWave( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close); + } + + /// + /// High wave candle over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult HighWave_Decimal() + { + return TACandle.CdlHighWave( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close); + } + + /// + /// Morning star over double inputs. This is the one pattern in the set that takes a penetration argument. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CandleIndicatorResult MorningStar_Double() + { + return TACandle.CdlMorningStar( + StartIdx, + EndIdx, + Series.Doubles.Open, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + PenetrationDouble); + } + + /// + /// Morning star over float inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CandleIndicatorResult MorningStar_Float() + { + return TACandle.CdlMorningStar( + StartIdx, + EndIdx, + Series.Singles.Open, + Series.Singles.High, + Series.Singles.Low, + Series.Singles.Close, + PenetrationSingle); + } + + /// + /// Morning star over decimal inputs. + /// + /// The pattern result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DecimalPrecision)] + public CandleIndicatorResult MorningStar_Decimal() + { + return TACandle.CdlMorningStar( + StartIdx, + EndIdx, + Series.Decimals.Open, + Series.Decimals.High, + Series.Decimals.Low, + Series.Decimals.Close, + PenetrationDecimal); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/MarketDataBenchmarkBase.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/MarketDataBenchmarkBase.cs new file mode 100644 index 00000000..a6065bd0 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/MarketDataBenchmarkBase.cs @@ -0,0 +1,75 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Benchmarks.Data; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// Shared plumbing for every indicator benchmark: the series length parameter, the generated market data and the +/// pre-allocated output buffers used by the allocation-free TAFunc path. +/// +/// +/// +/// Three output buffers are provided because no bound indicator writes more than three output series +/// (MACD and Bollinger Bands are the widest at three). +/// +/// +/// The buffers are allocated once in [GlobalSetup], i.e. outside the measured region. A benchmark named +/// *_TAFunc therefore measures the algorithm only; a benchmark named *_TAMath measures the algorithm +/// plus the result-object and output-array allocations that the ergonomic API performs on every call. +/// +/// +public abstract class MarketDataBenchmarkBase +{ + /// + /// Gets or sets the number of bars fed to the indicator. + /// + [Params(1_000, 10_000, 100_000)] + public int Length { get; set; } + + /// + /// Gets the generated market data for the current . + /// + protected MarketSeries Series { get; private set; } = MarketDataGenerator.Generate(2); + + /// + /// The first index handed to the indicator. Always zero: benchmarks always run the whole series. + /// + protected const int StartIdx = 0; + + /// + /// Gets the last index handed to the indicator, i.e. minus one. + /// + protected int EndIdx => Length - 1; + + /// + /// Gets the first pre-allocated output buffer. + /// + protected double[] Output0 { get; private set; } = []; + + /// + /// Gets the second pre-allocated output buffer. + /// + protected double[] Output1 { get; private set; } = []; + + /// + /// Gets the third pre-allocated output buffer. + /// + protected double[] Output2 { get; private set; } = []; + + /// + /// Generates the market data and allocates the output buffers. Call this from a [GlobalSetup] method. + /// + protected void InitializeMarketData() + { + Series = MarketDataGenerator.Generate(Length); + Output0 = new double[Length]; + Output1 = new double[Length]; + Output2 = new double[Length]; + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/MomentumBenchmarks.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/MomentumBenchmarks.cs new file mode 100644 index 00000000..bb5c5372 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/MomentumBenchmarks.cs @@ -0,0 +1,505 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// Momentum oscillators. +/// +/// +/// +/// As in the other suites, _TAFunc measures the algorithm with caller-supplied output buffers and +/// _TAMath measures the ergonomic API including its per-call allocations. Several of these indicators are +/// composites (MACD, StochRsi, Ppo, UltOsc) and allocate internal scratch arrays even on the TAFunc path; +/// the memory columns make that visible. +/// +/// +/// There is deliberately no Ratio column here. BenchmarkDotNet's default logical group +/// is (Job, Params), so a single Baseline = true would ratio every method in the class against +/// that one method β€” an EMA row would read as "3.7x slower" when what it measured was EMA against SMA, not +/// TAMath against TAFunc. Grouping per indicator cannot fix it either, because +/// BenchmarkLogicalGroupRule.ByCategory keys on the whole category set and the TAFunc / +/// TAMath categories put the two halves of a pair in different groups. Read the two rows of the same +/// indicator and divide the Mean column yourself; is the one +/// suite whose categories do line up, and it is the one that carries baselines. +/// +/// +[MemoryDiagnoser] +[CategoriesColumn] +[BenchmarkCategory(BenchmarkCategories.Momentum)] +public class MomentumBenchmarks : MarketDataBenchmarkBase +{ + private const int RsiPeriod = 14; + private const int MacdFast = 12; + private const int MacdSlow = 26; + private const int MacdSignal = 9; + private const int StochFastK = 5; + private const int StochSlowK = 3; + private const int StochSlowD = 3; + private const int StochRsiPeriod = 14; + private const int StochRsiFastK = 5; + private const int StochRsiFastD = 3; + private const int AdxPeriod = 14; + private const int CciPeriod = 14; + private const int MfiPeriod = 14; + private const int WillRPeriod = 14; + private const int PpoFast = 12; + private const int PpoSlow = 26; + private const int RocPeriod = 10; + private const int UltOscPeriod1 = 7; + private const int UltOscPeriod2 = 14; + private const int UltOscPeriod3 = 28; + private const int AroonPeriod = 14; + + /// + /// Generates the market data and allocates the output buffers. + /// + [GlobalSetup] + public void Setup() + { + InitializeMarketData(); + } + + /// + /// Relative strength index, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Rsi_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Relative strength index, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public RsiResult Rsi_TAMath() + { + return TAMath.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod); + } + + /// + /// Moving average convergence divergence, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Macd_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] macd = Output0; + double[] signal = Output1; + double[] histogram = Output2; + + return TAFunc.Macd( + StartIdx, + EndIdx, + Series.Doubles.Close, + MacdFast, + MacdSlow, + MacdSignal, + ref begIdx, + ref nbElement, + ref macd, + ref signal, + ref histogram); + } + + /// + /// Moving average convergence divergence, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public MacdResult Macd_TAMath() + { + return TAMath.Macd(StartIdx, EndIdx, Series.Doubles.Close, MacdFast, MacdSlow, MacdSignal); + } + + /// + /// Slow stochastic, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Stoch_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] slowK = Output0; + double[] slowD = Output1; + + return TAFunc.Stoch( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + StochFastK, + StochSlowK, + MAType.Sma, + StochSlowD, + MAType.Sma, + ref begIdx, + ref nbElement, + ref slowK, + ref slowD); + } + + /// + /// Slow stochastic, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public StochResult Stoch_TAMath() + { + return TAMath.Stoch( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + StochFastK, + StochSlowK, + MAType.Sma, + StochSlowD, + MAType.Sma); + } + + /// + /// Stochastic RSI, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode StochRsi_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] fastK = Output0; + double[] fastD = Output1; + + return TAFunc.StochRsi( + StartIdx, + EndIdx, + Series.Doubles.Close, + StochRsiPeriod, + StochRsiFastK, + StochRsiFastD, + MAType.Sma, + ref begIdx, + ref nbElement, + ref fastK, + ref fastD); + } + + /// + /// Stochastic RSI, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public StochRsiResult StochRsi_TAMath() + { + return TAMath.StochRsi(StartIdx, EndIdx, Series.Doubles.Close, StochRsiPeriod, StochRsiFastK, StochRsiFastD); + } + + /// + /// Average directional movement index, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Adx_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Adx( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AdxPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Average directional movement index, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public AdxResult Adx_TAMath() + { + return TAMath.Adx(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close, AdxPeriod); + } + + /// + /// Commodity channel index, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Cci_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Cci( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + CciPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Commodity channel index, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public CciResult Cci_TAMath() + { + return TAMath.Cci(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close, CciPeriod); + } + + /// + /// Money flow index, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Mfi_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Mfi( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + Series.Doubles.Volume, + MfiPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Money flow index, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public MfiResult Mfi_TAMath() + { + return TAMath.Mfi( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + Series.Doubles.Volume, + MfiPeriod); + } + + /// + /// Williams %R, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode WillR_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.WillR( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + WillRPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Williams %R, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public WillRResult WillR_TAMath() + { + return TAMath.WillR(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close, WillRPeriod); + } + + /// + /// Percentage price oscillator, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Ppo_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Ppo( + StartIdx, + EndIdx, + Series.Doubles.Close, + PpoFast, + PpoSlow, + MAType.Sma, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Percentage price oscillator, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public PpoResult Ppo_TAMath() + { + return TAMath.Ppo(StartIdx, EndIdx, Series.Doubles.Close, PpoFast, PpoSlow); + } + + /// + /// Rate of change, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Roc_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Roc(StartIdx, EndIdx, Series.Doubles.Close, RocPeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Rate of change, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public RocResult Roc_TAMath() + { + return TAMath.Roc(StartIdx, EndIdx, Series.Doubles.Close, RocPeriod); + } + + /// + /// Ultimate oscillator, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode UltOsc_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.UltOsc( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + UltOscPeriod1, + UltOscPeriod2, + UltOscPeriod3, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Ultimate oscillator, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public UltOscResult UltOsc_TAMath() + { + return TAMath.UltOsc( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + UltOscPeriod1, + UltOscPeriod2, + UltOscPeriod3); + } + + /// + /// Aroon, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Aroon_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] aroonDown = Output0; + double[] aroonUp = Output1; + + return TAFunc.Aroon( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + AroonPeriod, + ref begIdx, + ref nbElement, + ref aroonDown, + ref aroonUp); + } + + /// + /// Aroon, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public AroonResult Aroon_TAMath() + { + return TAMath.Aroon(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, AroonPeriod); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/NativeComparisonBenchmarks.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/NativeComparisonBenchmarks.cs new file mode 100644 index 00000000..161e8392 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/NativeComparisonBenchmarks.cs @@ -0,0 +1,750 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using TechnicalAnalysis.Benchmarks.Interop; +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// Head-to-head comparison between the managed TaLibStandard kernels and the original TA-Lib C library. +/// +/// +/// +/// This class only runs when is . Program +/// removes it from the runnable set otherwise, so the suite has no native dependency by default. +/// +/// +/// Both sides use caller-supplied output buffers allocated in [GlobalSetup], so the comparison is +/// algorithm against algorithm with no allocation noise on either side. The managed side deliberately uses +/// TAFunc rather than TAMath for exactly that reason. +/// +/// +/// [GlobalSetup] runs both implementations once and asserts they agree, so a "faster" result can never come +/// from computing the wrong thing. Benchmarks are grouped per indicator with the managed implementation as the +/// baseline, so the Ratio column reads directly as "native time / managed time". +/// +/// +[MemoryDiagnoser] +[CategoriesColumn] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[BenchmarkCategory(BenchmarkCategories.NativeComparison)] +public class NativeComparisonBenchmarks : MarketDataBenchmarkBase +{ + private const int SmaPeriod = 30; + private const int EmaPeriod = 30; + private const int RsiPeriod = 14; + private const int MacdFast = 12; + private const int MacdSlow = 26; + private const int MacdSignal = 9; + private const int BbandsPeriod = 20; + private const int AtrPeriod = 14; + private const int AdxPeriod = 14; + private const int StochFastK = 5; + private const int StochSlowK = 3; + private const int StochSlowD = 3; + + private double[] _native0 = []; + private double[] _native1 = []; + private double[] _native2 = []; + + /// + /// Generates the market data, allocates every output buffer and proves managed and native agree. + /// + /// + /// Thrown when the native library is unavailable, when either implementation reports a failure, or when the two + /// implementations disagree. + /// + [GlobalSetup] + public void Setup() + { + if (!NativeTaLib.IsAvailable) + { + throw new InvalidOperationException( + "Native TA-Lib is not available. NativeComparisonBenchmarks must not be scheduled in that case. " + + NativeTaLib.Diagnostics); + } + + InitializeMarketData(); + + _native0 = new double[Length]; + _native1 = new double[Length]; + _native2 = new double[Length]; + + VerifyEquivalence(); + } + + /// + /// Simple moving average, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Sma")] + public RetCode Sma_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Sma(StartIdx, EndIdx, Series.Doubles.Close, SmaPeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Simple moving average, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Sma")] + public int Sma_Native() + { + return NativeTaLib.Sma(StartIdx, EndIdx, Series.Doubles.Close, SmaPeriod, out _, out _, _native0); + } + + /// + /// Exponential moving average, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Ema")] + public RetCode Ema_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Ema(StartIdx, EndIdx, Series.Doubles.Close, EmaPeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Exponential moving average, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Ema")] + public int Ema_Native() + { + return NativeTaLib.Ema(StartIdx, EndIdx, Series.Doubles.Close, EmaPeriod, out _, out _, _native0); + } + + /// + /// Relative strength index, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Rsi")] + public RetCode Rsi_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Relative strength index, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Rsi")] + public int Rsi_Native() + { + return NativeTaLib.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod, out _, out _, _native0); + } + + /// + /// MACD, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Macd")] + public RetCode Macd_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] macd = Output0; + double[] signal = Output1; + double[] histogram = Output2; + + return TAFunc.Macd( + StartIdx, + EndIdx, + Series.Doubles.Close, + MacdFast, + MacdSlow, + MacdSignal, + ref begIdx, + ref nbElement, + ref macd, + ref signal, + ref histogram); + } + + /// + /// MACD, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Macd")] + public int Macd_Native() + { + return NativeTaLib.Macd( + StartIdx, + EndIdx, + Series.Doubles.Close, + MacdFast, + MacdSlow, + MacdSignal, + out _, + out _, + _native0, + _native1, + _native2); + } + + /// + /// Bollinger Bands, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Bbands")] + public RetCode Bbands_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] upper = Output0; + double[] middle = Output1; + double[] lower = Output2; + + return TAFunc.BollingerBands( + StartIdx, + EndIdx, + Series.Doubles.Close, + BbandsPeriod, + 2.0, + 2.0, + MAType.Sma, + ref begIdx, + ref nbElement, + ref upper, + ref middle, + ref lower); + } + + /// + /// Bollinger Bands, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Bbands")] + public int Bbands_Native() + { + return NativeTaLib.Bbands( + StartIdx, + EndIdx, + Series.Doubles.Close, + BbandsPeriod, + 2.0, + 2.0, + (int)MAType.Sma, + out _, + out _, + _native0, + _native1, + _native2); + } + + /// + /// Average true range, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Atr")] + public RetCode Atr_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Atr( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AtrPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Average true range, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Atr")] + public int Atr_Native() + { + return NativeTaLib.Atr( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AtrPeriod, + out _, + out _, + _native0); + } + + /// + /// Average directional movement index, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Adx")] + public RetCode Adx_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Adx( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AdxPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Average directional movement index, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Adx")] + public int Adx_Native() + { + return NativeTaLib.Adx( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AdxPeriod, + out _, + out _, + _native0); + } + + /// + /// Slow stochastic, managed kernel. + /// + /// The return code of the calculation. + [Benchmark(Baseline = true)] + [BenchmarkCategory("Stoch")] + public RetCode Stoch_Managed() + { + int begIdx = 0; + int nbElement = 0; + double[] slowK = Output0; + double[] slowD = Output1; + + return TAFunc.Stoch( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + StochFastK, + StochSlowK, + MAType.Sma, + StochSlowD, + MAType.Sma, + ref begIdx, + ref nbElement, + ref slowK, + ref slowD); + } + + /// + /// Slow stochastic, native TA-Lib C. + /// + /// The native return code. + [Benchmark] + [BenchmarkCategory("Stoch")] + public int Stoch_Native() + { + return NativeTaLib.Stoch( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + StochFastK, + StochSlowK, + (int)MAType.Sma, + StochSlowD, + (int)MAType.Sma, + out _, + out _, + _native0, + _native1); + } + + private void VerifyEquivalence() + { + VerifySingleOutput( + "SMA", + (out int beg, out int count, double[] buffer) => + { + int b = 0; + int n = 0; + double[] local = buffer; + RetCode code = TAFunc.Sma(StartIdx, EndIdx, Series.Doubles.Close, SmaPeriod, ref b, ref n, ref local); + beg = b; + count = n; + return code == RetCode.Success; + }, + (out int beg, out int count, double[] buffer) => + NativeTaLib.Sma(StartIdx, EndIdx, Series.Doubles.Close, SmaPeriod, out beg, out count, buffer) + == NativeTaLib.Success); + + VerifySingleOutput( + "EMA", + (out int beg, out int count, double[] buffer) => + { + int b = 0; + int n = 0; + double[] local = buffer; + RetCode code = TAFunc.Ema(StartIdx, EndIdx, Series.Doubles.Close, EmaPeriod, ref b, ref n, ref local); + beg = b; + count = n; + return code == RetCode.Success; + }, + (out int beg, out int count, double[] buffer) => + NativeTaLib.Ema(StartIdx, EndIdx, Series.Doubles.Close, EmaPeriod, out beg, out count, buffer) + == NativeTaLib.Success); + + VerifySingleOutput( + "RSI", + (out int beg, out int count, double[] buffer) => + { + int b = 0; + int n = 0; + double[] local = buffer; + RetCode code = TAFunc.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod, ref b, ref n, ref local); + beg = b; + count = n; + return code == RetCode.Success; + }, + (out int beg, out int count, double[] buffer) => + NativeTaLib.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod, out beg, out count, buffer) + == NativeTaLib.Success); + + VerifySingleOutput( + "ATR", + (out int beg, out int count, double[] buffer) => + { + int b = 0; + int n = 0; + double[] local = buffer; + RetCode code = TAFunc.Atr( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AtrPeriod, + ref b, + ref n, + ref local); + beg = b; + count = n; + return code == RetCode.Success; + }, + (out int beg, out int count, double[] buffer) => + NativeTaLib.Atr( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AtrPeriod, + out beg, + out count, + buffer) + == NativeTaLib.Success); + + VerifySingleOutput( + "ADX", + (out int beg, out int count, double[] buffer) => + { + int b = 0; + int n = 0; + double[] local = buffer; + RetCode code = TAFunc.Adx( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AdxPeriod, + ref b, + ref n, + ref local); + beg = b; + count = n; + return code == RetCode.Success; + }, + (out int beg, out int count, double[] buffer) => + NativeTaLib.Adx( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AdxPeriod, + out beg, + out count, + buffer) + == NativeTaLib.Success); + + VerifyMacd(); + VerifyBbands(); + VerifyStoch(); + } + + private void VerifyMacd() + { + int managedBeg = 0; + int managedCount = 0; + double[] managedMacd = new double[Length]; + double[] managedSignal = new double[Length]; + double[] managedHist = new double[Length]; + + RetCode managedCode = TAFunc.Macd( + StartIdx, + EndIdx, + Series.Doubles.Close, + MacdFast, + MacdSlow, + MacdSignal, + ref managedBeg, + ref managedCount, + ref managedMacd, + ref managedSignal, + ref managedHist); + + EnsureSucceeded("MACD", managedCode == RetCode.Success, isManaged: true); + + double[] nativeMacd = new double[Length]; + double[] nativeSignal = new double[Length]; + double[] nativeHist = new double[Length]; + + int nativeCode = NativeTaLib.Macd( + StartIdx, + EndIdx, + Series.Doubles.Close, + MacdFast, + MacdSlow, + MacdSignal, + out int nativeBeg, + out int nativeCount, + nativeMacd, + nativeSignal, + nativeHist); + + EnsureSucceeded("MACD", nativeCode == NativeTaLib.Success, isManaged: false); + + NativeEquivalence.AssertEquivalent("MACD (line)", managedBeg, managedCount, managedMacd, nativeBeg, nativeCount, nativeMacd); + NativeEquivalence.AssertEquivalent( + "MACD (signal)", + managedBeg, + managedCount, + managedSignal, + nativeBeg, + nativeCount, + nativeSignal); + NativeEquivalence.AssertEquivalent("MACD (hist)", managedBeg, managedCount, managedHist, nativeBeg, nativeCount, nativeHist); + } + + private void VerifyBbands() + { + int managedBeg = 0; + int managedCount = 0; + double[] managedUpper = new double[Length]; + double[] managedMiddle = new double[Length]; + double[] managedLower = new double[Length]; + + RetCode managedCode = TAFunc.BollingerBands( + StartIdx, + EndIdx, + Series.Doubles.Close, + BbandsPeriod, + 2.0, + 2.0, + MAType.Sma, + ref managedBeg, + ref managedCount, + ref managedUpper, + ref managedMiddle, + ref managedLower); + + EnsureSucceeded("BBANDS", managedCode == RetCode.Success, isManaged: true); + + double[] nativeUpper = new double[Length]; + double[] nativeMiddle = new double[Length]; + double[] nativeLower = new double[Length]; + + int nativeCode = NativeTaLib.Bbands( + StartIdx, + EndIdx, + Series.Doubles.Close, + BbandsPeriod, + 2.0, + 2.0, + (int)MAType.Sma, + out int nativeBeg, + out int nativeCount, + nativeUpper, + nativeMiddle, + nativeLower); + + EnsureSucceeded("BBANDS", nativeCode == NativeTaLib.Success, isManaged: false); + + NativeEquivalence.AssertEquivalent( + "BBANDS (upper)", + managedBeg, + managedCount, + managedUpper, + nativeBeg, + nativeCount, + nativeUpper); + NativeEquivalence.AssertEquivalent( + "BBANDS (middle)", + managedBeg, + managedCount, + managedMiddle, + nativeBeg, + nativeCount, + nativeMiddle); + NativeEquivalence.AssertEquivalent( + "BBANDS (lower)", + managedBeg, + managedCount, + managedLower, + nativeBeg, + nativeCount, + nativeLower); + } + + private void VerifyStoch() + { + int managedBeg = 0; + int managedCount = 0; + double[] managedSlowK = new double[Length]; + double[] managedSlowD = new double[Length]; + + RetCode managedCode = TAFunc.Stoch( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + StochFastK, + StochSlowK, + MAType.Sma, + StochSlowD, + MAType.Sma, + ref managedBeg, + ref managedCount, + ref managedSlowK, + ref managedSlowD); + + EnsureSucceeded("STOCH", managedCode == RetCode.Success, isManaged: true); + + double[] nativeSlowK = new double[Length]; + double[] nativeSlowD = new double[Length]; + + int nativeCode = NativeTaLib.Stoch( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + StochFastK, + StochSlowK, + (int)MAType.Sma, + StochSlowD, + (int)MAType.Sma, + out int nativeBeg, + out int nativeCount, + nativeSlowK, + nativeSlowD); + + EnsureSucceeded("STOCH", nativeCode == NativeTaLib.Success, isManaged: false); + + NativeEquivalence.AssertEquivalent( + "STOCH (slowK)", + managedBeg, + managedCount, + managedSlowK, + nativeBeg, + nativeCount, + nativeSlowK); + NativeEquivalence.AssertEquivalent( + "STOCH (slowD)", + managedBeg, + managedCount, + managedSlowD, + nativeBeg, + nativeCount, + nativeSlowD); + } + + private void VerifySingleOutput(string indicator, SingleOutputInvoker managed, SingleOutputInvoker @native) + { + double[] managedBuffer = new double[Length]; + double[] nativeBuffer = new double[Length]; + + EnsureSucceeded(indicator, managed(out int managedBeg, out int managedCount, managedBuffer), isManaged: true); + EnsureSucceeded(indicator, @native(out int nativeBeg, out int nativeCount, nativeBuffer), isManaged: false); + + NativeEquivalence.AssertEquivalent( + indicator, + managedBeg, + managedCount, + managedBuffer, + nativeBeg, + nativeCount, + nativeBuffer); + } + + private static void EnsureSucceeded(string indicator, bool succeeded, bool isManaged) + { + if (succeeded) + { + return; + } + + throw new InvalidOperationException( + $"{indicator}: the {(isManaged ? "managed" : "native")} implementation reported a failure during the " + + "equivalence check, so no timing can be trusted."); + } + + /// + /// Invokes one single-output indicator into the supplied buffer. + /// + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated output buffer. + /// when the call succeeded. + private delegate bool SingleOutputInvoker(out int outBegIdx, out int outNbElement, double[] buffer); +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/OverlapStudiesBenchmarks.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/OverlapStudiesBenchmarks.cs new file mode 100644 index 00000000..45b04ac0 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/OverlapStudiesBenchmarks.cs @@ -0,0 +1,353 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// Overlap studies: moving averages, envelopes and the parabolic SAR. +/// +/// +/// +/// Every indicator appears twice. The _TAFunc variant writes into buffers allocated in +/// [GlobalSetup] and therefore reports the pure algorithm cost with zero managed allocation. The +/// _TAMath variant calls the ergonomic API, which allocates one output array per output series plus one +/// result record per call; the delta between the two is the price of the convenient API. +/// +/// +/// There is deliberately no Ratio column here. BenchmarkDotNet's default logical group +/// is (Job, Params), so a single Baseline = true would ratio every method in the class against +/// that one method β€” an EMA row would read as "3.7x slower" when what it measured was EMA against SMA, not +/// TAMath against TAFunc. Grouping per indicator cannot fix it either, because +/// BenchmarkLogicalGroupRule.ByCategory keys on the whole category set and the TAFunc / +/// TAMath categories put the two halves of a pair in different groups. Read the two rows of the same +/// indicator and divide the Mean column yourself; is the one +/// suite whose categories do line up, and it is the one that carries baselines. +/// +/// +[MemoryDiagnoser] +[CategoriesColumn] +[BenchmarkCategory(BenchmarkCategories.OverlapStudies)] +public class OverlapStudiesBenchmarks : MarketDataBenchmarkBase +{ + private const int TimePeriod = 30; + private const int BollingerPeriod = 20; + private const int MidPointPeriod = 14; + private const int T3Period = 5; + private const double T3VFactor = 0.7; + + /// + /// Generates the market data and allocates the output buffers. + /// + [GlobalSetup] + public void Setup() + { + InitializeMarketData(); + } + + /// + /// Simple moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Sma_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Sma(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Simple moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public SmaResult Sma_TAMath() + { + return TAMath.Sma(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Exponential moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Ema_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Ema(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Exponential moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public EmaResult Ema_TAMath() + { + return TAMath.Ema(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Weighted moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Wma_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Wma(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Weighted moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public WmaResult Wma_TAMath() + { + return TAMath.Wma(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Double exponential moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Dema_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Dema(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Double exponential moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public DemaResult Dema_TAMath() + { + return TAMath.Dema(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Triple exponential moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Tema_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Tema(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Triple exponential moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public TemaResult Tema_TAMath() + { + return TAMath.Tema(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Triangular moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Trima_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Trima(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Triangular moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public TrimaResult Trima_TAMath() + { + return TAMath.Trima(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Kaufman adaptive moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Kama_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Kama(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// Kaufman adaptive moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public KamaResult Kama_TAMath() + { + return TAMath.Kama(StartIdx, EndIdx, Series.Doubles.Close, TimePeriod); + } + + /// + /// Tillson T3 moving average, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode T3_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.T3(StartIdx, EndIdx, Series.Doubles.Close, T3Period, T3VFactor, ref begIdx, ref nbElement, ref output); + } + + /// + /// Tillson T3 moving average, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public T3Result T3_TAMath() + { + return TAMath.T3(StartIdx, EndIdx, Series.Doubles.Close, T3Period, T3VFactor); + } + + /// + /// Bollinger Bands, allocation-free path. Three output series are written into pre-allocated buffers. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode BollingerBands_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] upper = Output0; + double[] middle = Output1; + double[] lower = Output2; + + return TAFunc.BollingerBands( + StartIdx, + EndIdx, + Series.Doubles.Close, + BollingerPeriod, + 2.0, + 2.0, + MAType.Sma, + ref begIdx, + ref nbElement, + ref upper, + ref middle, + ref lower); + } + + /// + /// Bollinger Bands, ergonomic path. Allocates three output arrays plus a result record. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public BollingerBandsResult BollingerBands_TAMath() + { + return TAMath.BollingerBands(StartIdx, EndIdx, Series.Doubles.Close, BollingerPeriod); + } + + /// + /// MidPoint over a period, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode MidPoint_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.MidPoint(StartIdx, EndIdx, Series.Doubles.Close, MidPointPeriod, ref begIdx, ref nbElement, ref output); + } + + /// + /// MidPoint over a period, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public MidPointResult MidPoint_TAMath() + { + return TAMath.MidPoint(StartIdx, EndIdx, Series.Doubles.Close, MidPointPeriod); + } + + /// + /// Parabolic SAR, allocation-free path. Note that the SAR implementation itself allocates a few tiny scratch + /// arrays internally, so this variant is not literally zero-allocation. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Sar_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Sar( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + 0.02, + 0.2, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Parabolic SAR, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public SarResult Sar_TAMath() + { + return TAMath.Sar(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/PrecisionBenchmarks.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/PrecisionBenchmarks.cs new file mode 100644 index 00000000..b53fb5b8 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/PrecisionBenchmarks.cs @@ -0,0 +1,208 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Functions; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// Double versus float on the same indicators, so the cost of the overloads is visible. +/// +/// +/// +/// The TAFunc kernels are written for only. Every float overload on +/// TAMath therefore widens its inputs into freshly allocated double[] arrays and then calls the same +/// kernel. A float benchmark consequently measures the double kernel plus one widening pass and one array +/// allocation per input series; it can never be faster than its double counterpart, and the memory columns +/// show exactly how much extra it costs. +/// +/// +/// This suite intentionally uses the ergonomic TAMath API for both precisions, because that is the only API +/// where a float entry point exists at all. Use and friends for the +/// allocation-free comparison. +/// +/// +/// There is deliberately no Ratio column here. A single Baseline = true would +/// ratio every row against one method β€” the default logical group is (Job, Params), not (indicator) β€” so a +/// float row would appear to be compared against its own double counterpart when it was in fact +/// compared against Sma_Double. Compare the Mean and Allocated columns of the two rows +/// of the same indicator instead. +/// +/// +[MemoryDiagnoser] +[CategoriesColumn] +[BenchmarkCategory(BenchmarkCategories.Precision)] +public class PrecisionBenchmarks : MarketDataBenchmarkBase +{ + private const int MaPeriod = 30; + private const int RsiPeriod = 14; + private const int AtrPeriod = 14; + private const int BollingerPeriod = 20; + + /// + /// Generates the market data. + /// + [GlobalSetup] + public void Setup() + { + InitializeMarketData(); + } + + /// + /// Simple moving average over double inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public SmaResult Sma_Double() + { + return TAMath.Sma(StartIdx, EndIdx, Series.Doubles.Close, MaPeriod); + } + + /// + /// Simple moving average over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public SmaResult Sma_Float() + { + return TAMath.Sma(StartIdx, EndIdx, Series.Singles.Close, MaPeriod); + } + + /// + /// Exponential moving average over double inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public EmaResult Ema_Double() + { + return TAMath.Ema(StartIdx, EndIdx, Series.Doubles.Close, MaPeriod); + } + + /// + /// Exponential moving average over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public EmaResult Ema_Float() + { + return TAMath.Ema(StartIdx, EndIdx, Series.Singles.Close, MaPeriod); + } + + /// + /// Relative strength index over double inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public RsiResult Rsi_Double() + { + return TAMath.Rsi(StartIdx, EndIdx, Series.Doubles.Close, RsiPeriod); + } + + /// + /// Relative strength index over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public RsiResult Rsi_Float() + { + return TAMath.Rsi(StartIdx, EndIdx, Series.Singles.Close, RsiPeriod); + } + + /// + /// MACD over double inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public MacdResult Macd_Double() + { + return TAMath.Macd(StartIdx, EndIdx, Series.Doubles.Close); + } + + /// + /// MACD over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public MacdResult Macd_Float() + { + return TAMath.Macd(StartIdx, EndIdx, Series.Singles.Close); + } + + /// + /// Bollinger Bands over double inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public BollingerBandsResult BollingerBands_Double() + { + return TAMath.BollingerBands(StartIdx, EndIdx, Series.Doubles.Close, BollingerPeriod); + } + + /// + /// Bollinger Bands over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public BollingerBandsResult BollingerBands_Float() + { + return TAMath.BollingerBands(StartIdx, EndIdx, Series.Singles.Close, BollingerPeriod); + } + + /// + /// Average true range over double inputs. Three input series must be widened on the float path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public AtrResult Atr_Double() + { + return TAMath.Atr(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close, AtrPeriod); + } + + /// + /// Average true range over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public AtrResult Atr_Float() + { + return TAMath.Atr(StartIdx, EndIdx, Series.Singles.High, Series.Singles.Low, Series.Singles.Close, AtrPeriod); + } + + /// + /// Correlation over double inputs. Two input series must be widened on the float path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.DoublePrecision)] + public CorrelResult Correl_Double() + { + return TAMath.Correl(StartIdx, EndIdx, Series.Doubles.Close, Series.ReferenceDoubles.Close, MaPeriod); + } + + /// + /// Correlation over float inputs. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.SinglePrecision)] + public CorrelResult Correl_Float() + { + return TAMath.Correl(StartIdx, EndIdx, Series.Singles.Close, Series.ReferenceSingles.Close, MaPeriod); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/VolatilityVolumeBenchmarks.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/VolatilityVolumeBenchmarks.cs new file mode 100644 index 00000000..03b1341b --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/VolatilityVolumeBenchmarks.cs @@ -0,0 +1,396 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +namespace TechnicalAnalysis.Benchmarks.Benchmarks; + +/// +/// Volatility indicators, volume indicators and the two-series statistic functions. +/// +/// +/// +/// Correl and Beta consume the primary close series and the correlated reference close series produced by +/// , so the statistics they compute are meaningful rather than degenerate. +/// +/// +/// There is deliberately no Ratio column here. BenchmarkDotNet's default logical group +/// is (Job, Params), so a single Baseline = true would ratio every method in the class against +/// that one method β€” an EMA row would read as "3.7x slower" when what it measured was EMA against SMA, not +/// TAMath against TAFunc. Grouping per indicator cannot fix it either, because +/// BenchmarkLogicalGroupRule.ByCategory keys on the whole category set and the TAFunc / +/// TAMath categories put the two halves of a pair in different groups. Read the two rows of the same +/// indicator and divide the Mean column yourself; is the one +/// suite whose categories do line up, and it is the one that carries baselines. +/// +/// +[MemoryDiagnoser] +[CategoriesColumn] +[BenchmarkCategory(BenchmarkCategories.VolatilityVolume)] +public class VolatilityVolumeBenchmarks : MarketDataBenchmarkBase +{ + private const int AtrPeriod = 14; + private const int NatrPeriod = 14; + private const int AdOscFast = 3; + private const int AdOscSlow = 10; + private const int StdDevPeriod = 20; + private const int VariancePeriod = 20; + private const int CorrelPeriod = 30; + private const int BetaPeriod = 5; + private const double NbDev = 1.0; + + /// + /// Generates the market data and allocates the output buffers. + /// + [GlobalSetup] + public void Setup() + { + InitializeMarketData(); + } + + /// + /// Average true range, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Atr_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Atr( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + AtrPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Average true range, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public AtrResult Atr_TAMath() + { + return TAMath.Atr(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close, AtrPeriod); + } + + /// + /// Normalized average true range, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Natr_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Natr( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + NatrPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Normalized average true range, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public NatrResult Natr_TAMath() + { + return TAMath.Natr(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close, NatrPeriod); + } + + /// + /// True range, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode TrueRange_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.TrueRange( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// True range, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public TrueRangeResult TrueRange_TAMath() + { + return TAMath.TrueRange(StartIdx, EndIdx, Series.Doubles.High, Series.Doubles.Low, Series.Doubles.Close); + } + + /// + /// On balance volume, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Obv_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Obv( + StartIdx, + EndIdx, + Series.Doubles.Close, + Series.Doubles.Volume, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// On balance volume, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public ObvResult Obv_TAMath() + { + return TAMath.Obv(StartIdx, EndIdx, Series.Doubles.Close, Series.Doubles.Volume); + } + + /// + /// Chaikin accumulation / distribution line, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Ad_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Ad( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + Series.Doubles.Volume, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Chaikin accumulation / distribution line, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public AdResult Ad_TAMath() + { + return TAMath.Ad( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + Series.Doubles.Volume); + } + + /// + /// Chaikin accumulation / distribution oscillator, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode AdOsc_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.AdOsc( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + Series.Doubles.Volume, + AdOscFast, + AdOscSlow, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Chaikin accumulation / distribution oscillator, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public AdOscResult AdOsc_TAMath() + { + return TAMath.AdOsc( + StartIdx, + EndIdx, + Series.Doubles.High, + Series.Doubles.Low, + Series.Doubles.Close, + Series.Doubles.Volume, + AdOscFast, + AdOscSlow); + } + + /// + /// Rolling standard deviation, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode StdDev_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.StdDev(StartIdx, EndIdx, Series.Doubles.Close, StdDevPeriod, NbDev, ref begIdx, ref nbElement, ref output); + } + + /// + /// Rolling standard deviation, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public StdDevResult StdDev_TAMath() + { + return TAMath.StdDev(StartIdx, EndIdx, Series.Doubles.Close, StdDevPeriod, NbDev); + } + + /// + /// Rolling variance, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Variance_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Variance( + StartIdx, + EndIdx, + Series.Doubles.Close, + VariancePeriod, + NbDev, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Rolling variance, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public VarianceResult Variance_TAMath() + { + return TAMath.Variance(StartIdx, EndIdx, Series.Doubles.Close, VariancePeriod, NbDev); + } + + /// + /// Pearson correlation between the primary and the reference instrument, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Correl_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Correl( + StartIdx, + EndIdx, + Series.Doubles.Close, + Series.ReferenceDoubles.Close, + CorrelPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Pearson correlation between the primary and the reference instrument, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public CorrelResult Correl_TAMath() + { + return TAMath.Correl(StartIdx, EndIdx, Series.Doubles.Close, Series.ReferenceDoubles.Close, CorrelPeriod); + } + + /// + /// Beta of the primary instrument against the reference instrument, allocation-free path. + /// + /// The return code of the calculation. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaFunc)] + public RetCode Beta_TAFunc() + { + int begIdx = 0; + int nbElement = 0; + double[] output = Output0; + return TAFunc.Beta( + StartIdx, + EndIdx, + Series.Doubles.Close, + Series.ReferenceDoubles.Close, + BetaPeriod, + ref begIdx, + ref nbElement, + ref output); + } + + /// + /// Beta of the primary instrument against the reference instrument, ergonomic path. + /// + /// The calculated result. + [Benchmark] + [BenchmarkCategory(BenchmarkCategories.TaMath)] + public BetaResult Beta_TAMath() + { + return TAMath.Beta(StartIdx, EndIdx, Series.Doubles.Close, Series.ReferenceDoubles.Close, BetaPeriod); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Configuration/TaLibBenchmarkConfig.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Configuration/TaLibBenchmarkConfig.cs new file mode 100644 index 00000000..e2f5a4a3 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Configuration/TaLibBenchmarkConfig.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Exporters.Json; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Order; +using BenchmarkDotNet.Reports; + +namespace TechnicalAnalysis.Benchmarks.Configuration; + +/// +/// The shared BenchmarkDotNet configuration for the TaLibStandard performance suite. +/// +/// +/// +/// The configuration deliberately declares no job. BenchmarkDotNet then falls back to Job.Default, which +/// means the standard command line switches (--job Dry, --job Short, --runtimes, ...) add +/// exactly one job instead of multiplying an already-declared one. +/// +/// +/// Exporters: GitHub-flavoured markdown (paste straight into an issue or a release note) and full JSON +/// (machine readable, for tracking regressions between releases). Both land in +/// BenchmarkDotNet.Artifacts/results next to the executable. +/// +/// +public sealed class TaLibBenchmarkConfig : ManualConfig +{ + /// + /// Initializes a new instance of the class. + /// + public TaLibBenchmarkConfig() + { + AddLogger(ConsoleLogger.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(StatisticColumn.OperationsPerSecond); + + // Also declared per class via [MemoryDiagnoser]; BenchmarkDotNet de-duplicates the singleton instance. + AddDiagnoser(MemoryDiagnoser.Default); + + AddExporter(MarkdownExporter.GitHub); + AddExporter(JsonExporter.Full); + + // Declared order keeps every "_TAFunc" / "_TAMath" and "_Managed" / "_Native" pair adjacent in the summary, + // which is what a reader of this suite actually wants to compare. + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.Declared, MethodOrderPolicy.Declared)); + + WithSummaryStyle(SummaryStyle.Default + .WithRatioStyle(RatioStyle.Trend) + .WithMaxParameterColumnWidth(24)); + + // Anchored to the executable rather than the current directory, so running the suite from the repository + // root does not drop a BenchmarkDotNet.Artifacts folder there. Overridable with --artifacts. + WithArtifactsPath(Path.Combine(AppContext.BaseDirectory, "BenchmarkDotNet.Artifacts")); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Data/DeterministicRandom.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Data/DeterministicRandom.cs new file mode 100644 index 00000000..37b79f3d --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Data/DeterministicRandom.cs @@ -0,0 +1,120 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Numerics; + +namespace TechnicalAnalysis.Benchmarks.Data; + +/// +/// A self-contained, fully deterministic pseudo random number generator (xoshiro256** seeded through SplitMix64). +/// +/// +/// +/// The BCL does not guarantee that a given seed produces the same sequence across +/// runtime versions. Benchmarks must be comparable across machines and across .NET releases, so the generator is +/// implemented here instead of being taken from the BCL. The algorithm is deliberately simple and allocation free. +/// +/// +/// This type is not thread safe. Each generated series creates its own instance. +/// +/// +public sealed class DeterministicRandom +{ + private ulong _s0; + private ulong _s1; + private ulong _s2; + private ulong _s3; + private double _spareGaussian; + private bool _hasSpareGaussian; + + /// + /// Initializes a new instance of the class. + /// + /// The seed. The same seed always yields the same sequence. + public DeterministicRandom(int seed) + { + ulong state = unchecked((ulong)seed + 0x9E3779B97F4A7C15UL); + _s0 = SplitMix64(ref state); + _s1 = SplitMix64(ref state); + _s2 = SplitMix64(ref state); + _s3 = SplitMix64(ref state); + } + + /// + /// Returns the next raw 64 bit sample of the generator. + /// + /// A uniformly distributed unsigned 64 bit integer. + public ulong NextUInt64() + { + unchecked + { + ulong result = BitOperations.RotateLeft(_s1 * 5UL, 7) * 9UL; + ulong t = _s1 << 17; + + _s2 ^= _s0; + _s3 ^= _s1; + _s1 ^= _s2; + _s0 ^= _s3; + _s2 ^= t; + _s3 = BitOperations.RotateLeft(_s3, 45); + + return result; + } + } + + /// + /// Returns the next uniformly distributed sample in the half open interval [0, 1). + /// + /// A uniformly distributed double in [0, 1). + public double NextDouble() + { + // 53 significant bits, the exact precision of a double mantissa. + return (NextUInt64() >> 11) * (1.0 / 9007199254740992.0); + } + + /// + /// Returns the next standard normal sample (mean 0, standard deviation 1) using the Marsaglia polar method. + /// + /// A normally distributed double. + public double NextGaussian() + { + if (_hasSpareGaussian) + { + _hasSpareGaussian = false; + return _spareGaussian; + } + + double u; + double v; + double s; + + do + { + u = (2.0 * NextDouble()) - 1.0; + v = (2.0 * NextDouble()) - 1.0; + s = (u * u) + (v * v); + } + while (s is <= 0.0 or >= 1.0); + + double factor = Math.Sqrt(-2.0 * Math.Log(s) / s); + _spareGaussian = v * factor; + _hasSpareGaussian = true; + + return u * factor; + } + + private static ulong SplitMix64(ref ulong state) + { + unchecked + { + state += 0x9E3779B97F4A7C15UL; + ulong z = state; + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL; + return z ^ (z >> 31); + } + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketDataGenerator.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketDataGenerator.cs new file mode 100644 index 00000000..0e6fdcef --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketDataGenerator.cs @@ -0,0 +1,243 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Collections.Concurrent; + +namespace TechnicalAnalysis.Benchmarks.Data; + +/// +/// Produces deterministic, fully offline OHLCV series for benchmarking. +/// +/// +/// +/// The close price follows a discretised geometric Brownian motion +/// C[i] = C[i-1] * exp((mu - sigma^2 / 2) * dt + sigma * sqrt(dt) * Z) with a trading-day time step. +/// The open gaps away from the previous close, and the high / low extend beyond the candle body by an exponential +/// wick whose size is itself a random draw, so candlestick pattern recognisers see realistic bodies and shadows +/// instead of degenerate bars. Volume is log-normal and correlated with the absolute return of the bar. +/// +/// +/// Everything is driven by with a fixed default seed, so two runs on two machines +/// see byte-identical inputs. No network access, no market data provider, no files. +/// +/// +public static class MarketDataGenerator +{ + /// + /// The default seed. Change it only if every published benchmark number is regenerated at the same time. + /// + public const int DefaultSeed = 20240217; + + /// + /// The price the primary instrument starts from. + /// + private const double InitialPrice = 100.0; + + /// + /// The price the correlated reference instrument starts from. + /// + private const double ReferenceInitialPrice = 250.0; + + /// + /// The annualised drift of the geometric Brownian motion. + /// + private const double Drift = 0.08; + + /// + /// The annualised volatility of the geometric Brownian motion. + /// + private const double Volatility = 0.25; + + /// + /// The number of trading days per year, i.e. the inverse of the time step. + /// + private const double TradingDaysPerYear = 252.0; + + /// + /// The instantaneous correlation between the primary and the reference instrument. + /// + private const double ReferenceCorrelation = 0.65; + + /// + /// The median traded volume of a bar. + /// + private const double MedianVolume = 1_000_000.0; + + /// + /// Number of decimal places every generated price is rounded to, so the decimal projection is lossless. + /// + private const int PriceDecimals = 4; + + private static readonly ConcurrentDictionary<(int Length, int Seed), MarketSeries> Cache = new(); + + /// + /// Generates (or returns a cached) market series of the requested length. + /// + /// The number of bars to generate. Must be at least two. + /// The seed. Defaults to . + /// A deterministic . + /// Thrown when is less than two. + /// + /// Results are cached per (length, seed) pair. Generation happens in [GlobalSetup], never inside a measured + /// method, so caching does not influence any timing; it only keeps repeated setups cheap. + /// + public static MarketSeries Generate(int length, int seed = DefaultSeed) + { + ArgumentOutOfRangeException.ThrowIfLessThan(length, 2); + + return Cache.GetOrAdd((length, seed), static key => GenerateCore(key.Length, key.Seed)); + } + + private static MarketSeries GenerateCore(int length, int seed) + { + DeterministicRandom random = new(seed); + + double dt = 1.0 / TradingDaysPerYear; + double barDrift = (Drift - (0.5 * Volatility * Volatility)) * dt; + double barVolatility = Volatility * Math.Sqrt(dt); + double crossVolatility = Math.Sqrt(1.0 - (ReferenceCorrelation * ReferenceCorrelation)); + + double[] open = new double[length]; + double[] high = new double[length]; + double[] low = new double[length]; + double[] close = new double[length]; + double[] volume = new double[length]; + + double[] refOpen = new double[length]; + double[] refHigh = new double[length]; + double[] refLow = new double[length]; + double[] refClose = new double[length]; + double[] refVolume = new double[length]; + + double price = InitialPrice; + double referencePrice = ReferenceInitialPrice; + + for (int i = 0; i < length; i++) + { + double shock = random.NextGaussian(); + double referenceShock = (ReferenceCorrelation * shock) + (crossVolatility * random.NextGaussian()); + + BuildBar(random, ref price, barDrift, barVolatility, shock, i, open, high, low, close, volume); + BuildBar( + random, + ref referencePrice, + barDrift, + barVolatility * 0.8, + referenceShock, + i, + refOpen, + refHigh, + refLow, + refClose, + refVolume); + } + + OhlcvSeries doubles = new(open, high, low, close, volume); + OhlcvSeries referenceDoubles = new(refOpen, refHigh, refLow, refClose, refVolume); + + return new MarketSeries( + seed, + doubles, + ToSingle(doubles), + ToDecimal(doubles), + referenceDoubles, + ToSingle(referenceDoubles)); + } + + private static void BuildBar( + DeterministicRandom random, + ref double price, + double barDrift, + double barVolatility, + double shock, + int index, + double[] open, + double[] high, + double[] low, + double[] close, + double[] volume) + { + double previousClose = price; + + // Overnight gap: a fraction of a daily move, so the open is close to but rarely equal to the previous close. + double gap = 0.20 * barVolatility * random.NextGaussian(); + double barOpen = previousClose * Math.Exp(gap); + double barClose = previousClose * Math.Exp(barDrift + (barVolatility * shock)); + + double bodyHigh = Math.Max(barOpen, barClose); + double bodyLow = Math.Min(barOpen, barClose); + + // Wicks are always non-negative and independent of the body direction. + double upperWick = Math.Abs(random.NextGaussian()) * barVolatility * 0.6; + double lowerWick = Math.Abs(random.NextGaussian()) * barVolatility * 0.6; + + double barHigh = bodyHigh * Math.Exp(upperWick); + double barLow = bodyLow * Math.Exp(-lowerWick); + + // Volume is log-normal and grows with the magnitude of the bar's return. + double relativeMove = Math.Abs((barClose / previousClose) - 1.0); + double barVolume = MedianVolume * Math.Exp(0.35 * random.NextGaussian()) * (1.0 + (8.0 * relativeMove)); + + barOpen = Math.Round(barOpen, PriceDecimals, MidpointRounding.AwayFromZero); + barClose = Math.Round(barClose, PriceDecimals, MidpointRounding.AwayFromZero); + barHigh = Math.Round(barHigh, PriceDecimals, MidpointRounding.AwayFromZero); + barLow = Math.Round(barLow, PriceDecimals, MidpointRounding.AwayFromZero); + + // Rounding must never break the OHLC invariants that every candlestick recogniser relies upon. + barHigh = Math.Max(barHigh, Math.Max(barOpen, barClose)); + barLow = Math.Min(barLow, Math.Min(barOpen, barClose)); + + open[index] = barOpen; + high[index] = barHigh; + low[index] = barLow; + close[index] = barClose; + volume[index] = Math.Round(barVolume / 100.0, MidpointRounding.AwayFromZero) * 100.0; + + price = barClose; + } + + private static OhlcvSeries ToSingle(OhlcvSeries source) + { + return new OhlcvSeries( + ToSingle(source.Open), + ToSingle(source.High), + ToSingle(source.Low), + ToSingle(source.Close), + ToSingle(source.Volume)); + } + + private static OhlcvSeries ToDecimal(OhlcvSeries source) + { + return new OhlcvSeries( + ToDecimal(source.Open), + ToDecimal(source.High), + ToDecimal(source.Low), + ToDecimal(source.Close), + ToDecimal(source.Volume)); + } + + private static float[] ToSingle(double[] source) + { + float[] result = new float[source.Length]; + for (int i = 0; i < source.Length; i++) + { + result[i] = (float)source[i]; + } + + return result; + } + + private static decimal[] ToDecimal(double[] source) + { + decimal[] result = new decimal[source.Length]; + for (int i = 0; i < source.Length; i++) + { + result[i] = (decimal)source[i]; + } + + return result; + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketSeries.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketSeries.cs new file mode 100644 index 00000000..998de135 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketSeries.cs @@ -0,0 +1,82 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Benchmarks.Data; + +/// +/// One synthetic market data set, exposed in the three numeric precisions the library supports plus a second, +/// correlated instrument used by the two-input indicators (Correl, Beta). +/// +/// +/// +/// , and are projections of the exact same +/// underlying series. The generator rounds every price to four decimal places so that the decimal projection is an +/// exact representation of the double projection; only the float projection loses information. That makes a +/// double / float / decimal comparison a pure cost comparison rather than a "different data" comparison. +/// +/// +public sealed class MarketSeries +{ + /// + /// Initializes a new instance of the class. + /// + /// The seed the series was generated from. + /// The primary instrument as doubles. + /// The primary instrument as floats. + /// The primary instrument as decimals. + /// The correlated reference instrument as doubles. + /// The correlated reference instrument as floats. + public MarketSeries( + int seed, + OhlcvSeries doubles, + OhlcvSeries singles, + OhlcvSeries decimals, + OhlcvSeries referenceDoubles, + OhlcvSeries referenceSingles) + { + Seed = seed; + Doubles = doubles; + Singles = singles; + Decimals = decimals; + ReferenceDoubles = referenceDoubles; + ReferenceSingles = referenceSingles; + } + + /// + /// Gets the seed the series was generated from. + /// + public int Seed { get; } + + /// + /// Gets the number of bars in the series. + /// + public int Length => Doubles.Length; + + /// + /// Gets the primary instrument projected onto . + /// + public OhlcvSeries Doubles { get; } + + /// + /// Gets the primary instrument projected onto . + /// + public OhlcvSeries Singles { get; } + + /// + /// Gets the primary instrument projected onto . + /// + public OhlcvSeries Decimals { get; } + + /// + /// Gets a second, correlated instrument projected onto , used by Correl and Beta. + /// + public OhlcvSeries ReferenceDoubles { get; } + + /// + /// Gets a second, correlated instrument projected onto , used by Correl and Beta. + /// + public OhlcvSeries ReferenceSingles { get; } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Data/OhlcvSeries.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Data/OhlcvSeries.cs new file mode 100644 index 00000000..9c697e25 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Data/OhlcvSeries.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Benchmarks.Data; + +/// +/// An open / high / low / close / volume series projected onto a single numeric type. +/// +/// The numeric type the series is projected onto (double, float or decimal). +/// +/// All five arrays always have the same length. Instances are produced by and are +/// treated as immutable by the benchmarks: an indicator must never write into its input. +/// +public sealed class OhlcvSeries +{ + /// + /// Initializes a new instance of the class. + /// + /// The open prices. + /// The high prices. + /// The low prices. + /// The close prices. + /// The traded volumes. + public OhlcvSeries(T[] open, T[] high, T[] low, T[] close, T[] volume) + { + Open = open; + High = high; + Low = low; + Close = close; + Volume = volume; + } + + /// + /// Gets the open prices. + /// + public T[] Open { get; } + + /// + /// Gets the high prices. + /// + public T[] High { get; } + + /// + /// Gets the low prices. + /// + public T[] Low { get; } + + /// + /// Gets the close prices. + /// + public T[] Close { get; } + + /// + /// Gets the traded volumes. + /// + public T[] Volume { get; } + + /// + /// Gets the number of bars in the series. + /// + public int Length => Close.Length; +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Diagnostics/BenchmarkSelfCheck.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Diagnostics/BenchmarkSelfCheck.cs new file mode 100644 index 00000000..89087e0e --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Diagnostics/BenchmarkSelfCheck.cs @@ -0,0 +1,186 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using System.Reflection; +using BenchmarkDotNet.Attributes; +using TechnicalAnalysis.Common; + +namespace TechnicalAnalysis.Benchmarks.Diagnostics; + +/// +/// Runs every benchmark method exactly once, outside BenchmarkDotNet, and checks that it actually succeeds. +/// +/// +/// +/// A benchmark that silently measures a validation failure looks fast and means nothing. This self check invokes +/// each [Benchmark] method once at the smallest configured series length and inspects the returned value: +/// a , an or a native return code must all report success, and +/// any thrown exception is reported. +/// +/// +/// Invoke it with -- --selfcheck. It is a correctness gate, never a measurement: no timing is produced or +/// implied. +/// +/// +public static class BenchmarkSelfCheck +{ + /// + /// The command line flag that triggers the self check. + /// + public const string Flag = "--selfcheck"; + + /// + /// The series length used by the self check. The smallest configured [Params] value keeps it instant. + /// + private const int SelfCheckLength = 1_000; + + /// + /// Runs the self check over the supplied benchmark types. + /// + /// The benchmark classes to check. + /// Zero when every benchmark succeeded, one otherwise. + public static int Run(IReadOnlyList benchmarkTypes) + { + ArgumentNullException.ThrowIfNull(benchmarkTypes); + + int checkedCount = 0; + List failures = []; + + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + "Self check: invoking every benchmark once with Length = {0}.", + SelfCheckLength)); + Console.WriteLine(); + + foreach (Type type in benchmarkTypes) + { + int typeFailures = failures.Count; + CheckType(type, ref checkedCount, failures); + + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + " {0,-32} {1}", + type.Name, + failures.Count == typeFailures ? "OK" : "FAILED")); + } + + Console.WriteLine(); + + if (failures.Count == 0) + { + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + "Self check passed: {0} benchmark method(s) all reported success.", + checkedCount)); + return 0; + } + + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + "Self check FAILED: {0} of {1} benchmark method(s) did not report success.", + failures.Count, + checkedCount)); + + foreach (string failure in failures) + { + Console.WriteLine(" " + failure); + } + + return 1; + } + + private static void CheckType(Type type, ref int checkedCount, List failures) + { + object? instance; + + try + { + instance = Activator.CreateInstance(type); + } + catch (Exception ex) + { + failures.Add($"{type.Name}: could not be instantiated: {Describe(ex)}"); + return; + } + + if (instance is null) + { + failures.Add($"{type.Name}: could not be instantiated."); + return; + } + + PropertyInfo? lengthProperty = type.GetProperty("Length", BindingFlags.Public | BindingFlags.Instance); + lengthProperty?.SetValue(instance, SelfCheckLength); + + MethodInfo? setup = type + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .FirstOrDefault(static method => method.GetCustomAttribute() is not null); + + try + { + setup?.Invoke(instance, null); + } + catch (Exception ex) + { + failures.Add($"{type.Name}: [GlobalSetup] threw {Describe(ex)}"); + return; + } + + foreach (MethodInfo method in type + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(static method => method.GetCustomAttribute() is not null) + .Where(static method => method.GetParameters().Length == 0)) + { + checkedCount++; + + try + { + object? result = method.Invoke(instance, null); + string? problem = Validate(result); + + if (problem is not null) + { + failures.Add($"{type.Name}.{method.Name}: {problem}"); + } + } + catch (Exception ex) + { + failures.Add($"{type.Name}.{method.Name}: threw {Describe(ex)}"); + } + } + } + + private static string? Validate(object? result) + { + return result switch + { + null => "returned null", + RetCode code when code != RetCode.Success => $"returned RetCode.{code}", + RetCode => null, + IndicatorResult indicator when indicator.RetCode != RetCode.Success => + $"result carries RetCode.{indicator.RetCode}", + IndicatorResult indicator when indicator.NBElement <= 0 => + $"result carries NBElement = {indicator.NBElement.ToString(CultureInfo.InvariantCulture)}", + IndicatorResult => null, + int nativeCode when nativeCode != 0 => + $"native call returned TA_RetCode {nativeCode.ToString(CultureInfo.InvariantCulture)}", + _ => null + }; + } + + private static string Describe(Exception exception) + { + Exception effective = exception; + + if (exception is TargetInvocationException { InnerException: { } inner }) + { + effective = inner; + } + + return $"{effective.GetType().Name}: {effective.Message}"; + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeEquivalence.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeEquivalence.cs new file mode 100644 index 00000000..2ebc25ca --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeEquivalence.cs @@ -0,0 +1,126 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; + +namespace TechnicalAnalysis.Benchmarks.Interop; + +/// +/// Verifies that the managed TaLibStandard implementation and the native TA-Lib C implementation compute the same +/// thing before either of them is timed. +/// +/// +/// +/// A benchmark that is "faster" because it computes the wrong answer is worse than no benchmark at all. Every +/// head-to-head pair in NativeComparisonBenchmarks is therefore validated in [GlobalSetup]: the +/// alignment metadata (outBegIdx and outNBElement) must match exactly and every produced value must +/// agree within tolerance. A mismatch throws, which BenchmarkDotNet surfaces as a failed benchmark. +/// +/// +/// Both APIs use the same output convention: the buffer is filled from index 0, and output element k +/// corresponds to input index outBegIdx + k for k in [0, outNBElement). Elements at or beyond +/// outNBElement are meaningless and are never compared. +/// +/// +public static class NativeEquivalence +{ + /// + /// The default relative tolerance. TA-Lib C and TaLibStandard run the same recurrences in the same order, so + /// results normally agree to the last few bits; this leaves room for compiler-level reassociation only. + /// + public const double DefaultTolerance = 1e-9; + + /// + /// Asserts that two implementations produced the same aligned output series. + /// + /// The indicator name, used in the failure message. + /// The managed outBegIdx. + /// The managed outNBElement. + /// The managed output buffer, filled from index zero. + /// The native outBegIdx. + /// The native outNBElement. + /// The native output buffer, filled from index zero. + /// The relative tolerance. Defaults to . + /// Thrown when the two implementations disagree. + public static void AssertEquivalent( + string indicator, + int managedBegIdx, + int managedCount, + double[] managedValues, + int nativeBegIdx, + int nativeCount, + double[] nativeValues, + double tolerance = DefaultTolerance) + { + ArgumentNullException.ThrowIfNull(managedValues); + ArgumentNullException.ThrowIfNull(nativeValues); + + if (managedBegIdx != nativeBegIdx) + { + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "{0}: output alignment differs. Managed outBegIdx = {1}, native outBegIdx = {2}. " + + "Comparing the timings would be meaningless.", + indicator, + managedBegIdx, + nativeBegIdx)); + } + + if (managedCount != nativeCount) + { + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "{0}: output length differs. Managed outNBElement = {1}, native outNBElement = {2}.", + indicator, + managedCount, + nativeCount)); + } + + for (int i = 0; i < managedCount; i++) + { + double managed = managedValues[i]; + double @native = nativeValues[i]; + + if (AreClose(managed, @native, tolerance)) + { + continue; + } + + throw new InvalidOperationException(string.Format( + CultureInfo.InvariantCulture, + "{0}: values diverge at output index {1} (input index {2}). Managed = {3:R}, native = {4:R}, " + + "absolute difference = {5:R}, tolerance = {6:R}.", + indicator, + i, + managedBegIdx + i, + managed, + @native, + Math.Abs(managed - @native), + tolerance)); + } + } + + private static bool AreClose(double left, double right, double tolerance) + { + if (double.IsNaN(left) && double.IsNaN(right)) + { + return true; + } + + if (double.IsNaN(left) || double.IsNaN(right)) + { + return false; + } + + if (left.Equals(right)) + { + return true; + } + + double scale = Math.Max(1.0, Math.Max(Math.Abs(left), Math.Abs(right))); + return Math.Abs(left - right) <= tolerance * scale; + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeTaLib.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeTaLib.cs new file mode 100644 index 00000000..6cdf0725 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeTaLib.cs @@ -0,0 +1,772 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; + +namespace TechnicalAnalysis.Benchmarks.Interop; + +/// +/// An opt-in P/Invoke bridge to the original TA-Lib C library. +/// +/// +/// +/// The bridge is entirely optional. probes for the native library once, never throws and +/// returns when nothing suitable is found; the rest of the benchmark suite then runs +/// unchanged with zero native dependencies. +/// +/// +/// Only functions whose C signature is certain are bound here. Every one of them is declared in +/// ta_func.h of the upstream TA-Lib distribution with the same shape: leading startIdx / +/// endIdx, then the const double[] inputs, then the optIn* parameters, then +/// int *outBegIdx and int *outNBElement, then the double[] output buffers. The return type is +/// the C enum TA_RetCode, which is marshalled as (TA_SUCCESS is 0). The calling +/// convention is cdecl on every supported platform. +/// +/// +/// The TA_MAType C enum uses the same ordinal order as TechnicalAnalysis.Common.MAType +/// (SMA, EMA, WMA, DEMA, TEMA, TRIMA, KAMA, MAMA, T3), so a plain cast to is correct. +/// +/// +public static class NativeTaLib +{ + /// + /// The value of TA_SUCCESS in the C TA_RetCode enum. + /// + public const int Success = 0; + + /// + /// The logical name used by every [DllImport] below. It is never resolved by the default loader: the + /// registered always answers with the handle discovered by . + /// + private const string LogicalLibraryName = "talib-native"; + + /// + /// Environment variable that overrides discovery with an explicit file name or absolute path. + /// + private const string OverrideEnvironmentVariable = "TALIB_NATIVE_LIBRARY"; + + /// + /// Library names handed to the platform loader, which applies the usual lib prefix and + /// .so / .dylib / .dll suffix conventions itself. + /// + private static readonly string[] CandidateNames = + [ + "ta-lib", + "ta_lib", + "libta-lib", + "libta_lib", + "ta_libc", + "ta_libc_cdr", + "libta-lib.so.0", + "libta_lib.so.0" + ]; + + /// + /// Absolute paths tried after the plain names, covering the default Homebrew, MacPorts and autotools prefixes. + /// + private static readonly string[] CandidatePaths = + [ + "/opt/homebrew/lib/libta-lib.dylib", + "/opt/homebrew/lib/libta_lib.dylib", + "/usr/local/lib/libta-lib.dylib", + "/usr/local/lib/libta_lib.dylib", + "/opt/local/lib/libta-lib.dylib", + "/usr/local/lib/libta-lib.so", + "/usr/local/lib/libta_lib.so", + "/usr/lib/libta-lib.so", + "/usr/lib/libta_lib.so", + "/usr/lib/x86_64-linux-gnu/libta-lib.so", + "/usr/lib/aarch64-linux-gnu/libta-lib.so" + ]; + + private static readonly Lock SyncRoot = new(); + + private static bool _probed; + private static bool _available; + private static IntPtr _handle; + private static string _resolvedName = ""; + private static string _diagnostics = "not probed yet"; + + static NativeTaLib() + { + // Registering the resolver here (rather than lazily) guarantees it is in place before the CLR resolves any + // of the [DllImport] entries below, because touching any static member runs this constructor first. + try + { + NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), ResolveLibrary); + } + catch (InvalidOperationException) + { + // A resolver was already registered for this assembly. Harmless: discovery still works through it or + // through the default loader, and a failure to bind simply leaves IsAvailable false. + } + } + + /// + /// Gets a value indicating whether the native TA-Lib C library was found, loaded and successfully initialised. + /// + /// + /// This property never throws. The first access performs the probe; subsequent accesses are a field read. + /// + public static bool IsAvailable + { + get + { + Probe(); + return _available; + } + } + + /// + /// Gets the name or path the native library was resolved from, or <none> when it was not found. + /// + public static string ResolvedName + { + get + { + Probe(); + return _resolvedName; + } + } + + /// + /// Gets a human readable description of what the probe tried and what happened, for the startup banner. + /// + public static string Diagnostics + { + get + { + Probe(); + return _diagnostics; + } + } + + /// + /// Calls TA_SMA. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The input series. + /// The averaging period. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated output buffer. + /// The C TA_RetCode; means success. + public static unsafe int Sma( + int startIdx, + int endIdx, + double[] inReal, + int optInTimePeriod, + out int outBegIdx, + out int outNbElement, + double[] outReal) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pIn = inReal) + fixed (double* pOut = outReal) + { + retCode = TA_SMA(startIdx, endIdx, pIn, optInTimePeriod, &begIdx, &nbElement, pOut); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_EMA. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The input series. + /// The averaging period. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated output buffer. + /// The C TA_RetCode; means success. + public static unsafe int Ema( + int startIdx, + int endIdx, + double[] inReal, + int optInTimePeriod, + out int outBegIdx, + out int outNbElement, + double[] outReal) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pIn = inReal) + fixed (double* pOut = outReal) + { + retCode = TA_EMA(startIdx, endIdx, pIn, optInTimePeriod, &begIdx, &nbElement, pOut); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_RSI. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The input series. + /// The RSI period. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated output buffer. + /// The C TA_RetCode; means success. + public static unsafe int Rsi( + int startIdx, + int endIdx, + double[] inReal, + int optInTimePeriod, + out int outBegIdx, + out int outNbElement, + double[] outReal) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pIn = inReal) + fixed (double* pOut = outReal) + { + retCode = TA_RSI(startIdx, endIdx, pIn, optInTimePeriod, &begIdx, &nbElement, pOut); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_MACD. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The input series. + /// The fast EMA period. + /// The slow EMA period. + /// The signal EMA period. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated MACD line buffer. + /// The caller-allocated signal line buffer. + /// The caller-allocated histogram buffer. + /// The C TA_RetCode; means success. + public static unsafe int Macd( + int startIdx, + int endIdx, + double[] inReal, + int optInFastPeriod, + int optInSlowPeriod, + int optInSignalPeriod, + out int outBegIdx, + out int outNbElement, + double[] outMacd, + double[] outMacdSignal, + double[] outMacdHist) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pIn = inReal) + fixed (double* pMacd = outMacd) + fixed (double* pSignal = outMacdSignal) + fixed (double* pHist = outMacdHist) + { + retCode = TA_MACD( + startIdx, + endIdx, + pIn, + optInFastPeriod, + optInSlowPeriod, + optInSignalPeriod, + &begIdx, + &nbElement, + pMacd, + pSignal, + pHist); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_BBANDS. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The input series. + /// The averaging period. + /// The number of standard deviations for the upper band. + /// The number of standard deviations for the lower band. + /// The moving average type, ordinal-compatible with MAType. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated upper band buffer. + /// The caller-allocated middle band buffer. + /// The caller-allocated lower band buffer. + /// The C TA_RetCode; means success. + public static unsafe int Bbands( + int startIdx, + int endIdx, + double[] inReal, + int optInTimePeriod, + double optInNbDevUp, + double optInNbDevDn, + int optInMaType, + out int outBegIdx, + out int outNbElement, + double[] outUpper, + double[] outMiddle, + double[] outLower) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pIn = inReal) + fixed (double* pUpper = outUpper) + fixed (double* pMiddle = outMiddle) + fixed (double* pLower = outLower) + { + retCode = TA_BBANDS( + startIdx, + endIdx, + pIn, + optInTimePeriod, + optInNbDevUp, + optInNbDevDn, + optInMaType, + &begIdx, + &nbElement, + pUpper, + pMiddle, + pLower); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_ATR. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The high price series. + /// The low price series. + /// The close price series. + /// The averaging period. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated output buffer. + /// The C TA_RetCode; means success. + public static unsafe int Atr( + int startIdx, + int endIdx, + double[] inHigh, + double[] inLow, + double[] inClose, + int optInTimePeriod, + out int outBegIdx, + out int outNbElement, + double[] outReal) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pHigh = inHigh) + fixed (double* pLow = inLow) + fixed (double* pClose = inClose) + fixed (double* pOut = outReal) + { + retCode = TA_ATR(startIdx, endIdx, pHigh, pLow, pClose, optInTimePeriod, &begIdx, &nbElement, pOut); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_ADX. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The high price series. + /// The low price series. + /// The close price series. + /// The averaging period. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated output buffer. + /// The C TA_RetCode; means success. + public static unsafe int Adx( + int startIdx, + int endIdx, + double[] inHigh, + double[] inLow, + double[] inClose, + int optInTimePeriod, + out int outBegIdx, + out int outNbElement, + double[] outReal) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pHigh = inHigh) + fixed (double* pLow = inLow) + fixed (double* pClose = inClose) + fixed (double* pOut = outReal) + { + retCode = TA_ADX(startIdx, endIdx, pHigh, pLow, pClose, optInTimePeriod, &begIdx, &nbElement, pOut); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + /// + /// Calls TA_STOCH. + /// + /// The first index of the input to process. + /// The last index of the input to process. + /// The high price series. + /// The low price series. + /// The close price series. + /// The fast %K period. + /// The slow %K smoothing period. + /// The slow %K moving average type. + /// The slow %D smoothing period. + /// The slow %D moving average type. + /// Receives the input index the first output element corresponds to. + /// Receives the number of valid output elements. + /// The caller-allocated slow %K buffer. + /// The caller-allocated slow %D buffer. + /// The C TA_RetCode; means success. + public static unsafe int Stoch( + int startIdx, + int endIdx, + double[] inHigh, + double[] inLow, + double[] inClose, + int optInFastKPeriod, + int optInSlowKPeriod, + int optInSlowKMaType, + int optInSlowDPeriod, + int optInSlowDMaType, + out int outBegIdx, + out int outNbElement, + double[] outSlowK, + double[] outSlowD) + { + int begIdx = 0; + int nbElement = 0; + int retCode; + + fixed (double* pHigh = inHigh) + fixed (double* pLow = inLow) + fixed (double* pClose = inClose) + fixed (double* pSlowK = outSlowK) + fixed (double* pSlowD = outSlowD) + { + retCode = TA_STOCH( + startIdx, + endIdx, + pHigh, + pLow, + pClose, + optInFastKPeriod, + optInSlowKPeriod, + optInSlowKMaType, + optInSlowDPeriod, + optInSlowDMaType, + &begIdx, + &nbElement, + pSlowK, + pSlowD); + } + + outBegIdx = begIdx; + outNbElement = nbElement; + return retCode; + } + + private static IntPtr ResolveLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + { + return string.Equals(libraryName, LogicalLibraryName, StringComparison.Ordinal) ? _handle : IntPtr.Zero; + } + + private static void Probe() + { + if (Volatile.Read(ref _probed)) + { + return; + } + + lock (SyncRoot) + { + if (_probed) + { + return; + } + + StringBuilder log = new(); + + try + { + ProbeCore(log); + } +#pragma warning disable CA1031 // Discovery must never propagate: the whole point is that the suite degrades gracefully. + catch (Exception ex) +#pragma warning restore CA1031 + { + _available = false; + _handle = IntPtr.Zero; + log.Append(CultureInfo.InvariantCulture, $"unexpected failure: {ex.GetType().Name}: {ex.Message}"); + } + + _diagnostics = log.ToString(); + Volatile.Write(ref _probed, true); + } + } + + private static void ProbeCore(StringBuilder log) + { + string? overridden = Environment.GetEnvironmentVariable(OverrideEnvironmentVariable); + List attempted = []; + + if (!string.IsNullOrWhiteSpace(overridden)) + { + attempted.Add(overridden); + } + + attempted.AddRange(CandidateNames); + attempted.AddRange(CandidatePaths); + + foreach (string candidate in attempted) + { + if (!TryLoad(candidate, out IntPtr handle)) + { + continue; + } + + // A library that loads but has no TA_Initialize export is not TA-Lib. + if (!NativeLibrary.TryGetExport(handle, "TA_Initialize", out IntPtr initialize)) + { + NativeLibrary.Free(handle); + log.Append(CultureInfo.InvariantCulture, $"'{candidate}' loaded but exports no TA_Initialize; ignored. "); + continue; + } + + // Called through the export pointer of *this* candidate, never through the [DllImport] stub. + // Invoking the stub would make the CLR run the resolver once and cache the resolved module and + // function pointer for the lifetime of the process; the next candidate would then jump to that + // cached address, which by then points into a library this loop has already freed. Calling the + // pointer directly keeps each candidate self-contained, so a library that loads but fails + // TA_Initialize costs an ignored candidate rather than an access violation. + int retCode = InvokeInitialize(initialize); + if (retCode != Success) + { + NativeLibrary.Free(handle); + log.Append(CultureInfo.InvariantCulture, $"'{candidate}': TA_Initialize returned {retCode}; ignored. "); + continue; + } + + // Published only now that the candidate is known good, so the resolver can never hand a + // [DllImport] a handle that is about to be freed. + _handle = handle; + _resolvedName = candidate; + + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; + _available = true; + log.Append(CultureInfo.InvariantCulture, $"resolved '{candidate}', TA_Initialize succeeded."); + return; + } + + _available = false; + _handle = IntPtr.Zero; + log.Append(CultureInfo.InvariantCulture, $"probed {attempted.Count} candidate name(s)/path(s), none loaded."); + } + + /// + /// Calls a resolved TA_Initialize export through its address, bypassing the P/Invoke stub and the + /// per-process caching that comes with it. + /// + /// The address of TA_Initialize in the candidate library. + /// The C TA_RetCode. + private static unsafe int InvokeInitialize(IntPtr entryPoint) + { + return ((delegate* unmanaged[Cdecl])entryPoint)(); + } + + private static bool TryLoad(string candidate, out IntPtr handle) + { + handle = IntPtr.Zero; + + try + { + if (Path.IsPathRooted(candidate)) + { + return File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out handle); + } + + return NativeLibrary.TryLoad( + candidate, + Assembly.GetExecutingAssembly(), + DllImportSearchPath.SafeDirectories | DllImportSearchPath.UserDirectories, + out handle); + } +#pragma warning disable CA1031 // A malformed candidate must not abort the probe. + catch (Exception) +#pragma warning restore CA1031 + { + handle = IntPtr.Zero; + return false; + } + } + + private static void OnProcessExit(object? sender, EventArgs e) + { + try + { + if (_available) + { + _available = false; + _ = TA_Shutdown(); + } + } +#pragma warning disable CA1031 // Nothing useful can be done at process exit. + catch (Exception) +#pragma warning restore CA1031 + { + // Ignored. + } + } + + [DllImport(LogicalLibraryName, EntryPoint = "TA_Shutdown", CallingConvention = CallingConvention.Cdecl)] + private static extern int TA_Shutdown(); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_SMA", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_SMA( + int startIdx, + int endIdx, + double* inReal, + int optInTimePeriod, + int* outBegIdx, + int* outNBElement, + double* outReal); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_EMA", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_EMA( + int startIdx, + int endIdx, + double* inReal, + int optInTimePeriod, + int* outBegIdx, + int* outNBElement, + double* outReal); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_RSI", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_RSI( + int startIdx, + int endIdx, + double* inReal, + int optInTimePeriod, + int* outBegIdx, + int* outNBElement, + double* outReal); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_MACD", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_MACD( + int startIdx, + int endIdx, + double* inReal, + int optInFastPeriod, + int optInSlowPeriod, + int optInSignalPeriod, + int* outBegIdx, + int* outNBElement, + double* outMACD, + double* outMACDSignal, + double* outMACDHist); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_BBANDS", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_BBANDS( + int startIdx, + int endIdx, + double* inReal, + int optInTimePeriod, + double optInNbDevUp, + double optInNbDevDn, + int optInMAType, + int* outBegIdx, + int* outNBElement, + double* outRealUpperBand, + double* outRealMiddleBand, + double* outRealLowerBand); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_ATR", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_ATR( + int startIdx, + int endIdx, + double* inHigh, + double* inLow, + double* inClose, + int optInTimePeriod, + int* outBegIdx, + int* outNBElement, + double* outReal); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_ADX", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_ADX( + int startIdx, + int endIdx, + double* inHigh, + double* inLow, + double* inClose, + int optInTimePeriod, + int* outBegIdx, + int* outNBElement, + double* outReal); + + [DllImport(LogicalLibraryName, EntryPoint = "TA_STOCH", CallingConvention = CallingConvention.Cdecl)] + private static extern unsafe int TA_STOCH( + int startIdx, + int endIdx, + double* inHigh, + double* inLow, + double* inClose, + int optInFastK_Period, + int optInSlowK_Period, + int optInSlowK_MAType, + int optInSlowD_Period, + int optInSlowD_MAType, + int* outBegIdx, + int* outNBElement, + double* outSlowK, + double* outSlowD); +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/Program.cs b/benchmarks/TechnicalAnalysis.Benchmarks/Program.cs new file mode 100644 index 00000000..ffd95b6b --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/Program.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using System.Reflection; +using System.Runtime; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Running; +using TechnicalAnalysis.Benchmarks.Benchmarks; +using TechnicalAnalysis.Benchmarks.Configuration; +using TechnicalAnalysis.Benchmarks.Diagnostics; +using TechnicalAnalysis.Benchmarks.Interop; + +namespace TechnicalAnalysis.Benchmarks; + +/// +/// Entry point of the TaLibStandard performance benchmark suite. +/// +/// +/// +/// All standard BenchmarkDotNet command line arguments are passed straight through to +/// , so --list flat, --filter, --anyCategories, --job, +/// --exporters and friends all work as documented upstream. +/// +/// +/// One project-specific flag is recognised before the switcher runs: --selfcheck invokes every benchmark +/// once and asserts it reports success, without producing any timing. +/// +/// +public static class Program +{ + /// + /// Runs the suite. + /// + /// The BenchmarkDotNet command line arguments. + /// Zero on success. + public static int Main(string[] args) + { + args ??= []; + + bool nativeAvailable = NativeTaLib.IsAvailable; + PrintBanner(nativeAvailable); + + Type[] runnableTypes = GetRunnableBenchmarkTypes(nativeAvailable); + if (runnableTypes.Length == 0) + { + Console.WriteLine("No benchmark types were discovered. Nothing to do."); + return 0; + } + + if (args.Contains(BenchmarkSelfCheck.Flag, StringComparer.OrdinalIgnoreCase)) + { + return BenchmarkSelfCheck.Run(runnableTypes); + } + + IConfig config = new TaLibBenchmarkConfig(); + BenchmarkSwitcher.FromTypes(runnableTypes).Run(args, config); + + return 0; + } + + /// + /// Returns every benchmark class that should be offered to . + /// + /// Whether the native TA-Lib C library was found. + /// The runnable benchmark types, in a stable alphabetical order. + /// + /// is excluded when the native library is missing, so that + /// --list, --filter * and an unattended full run never attempt something that cannot work. + /// + private static Type[] GetRunnableBenchmarkTypes(bool nativeAvailable) + { + IEnumerable candidates = Assembly.GetExecutingAssembly() + .GetExportedTypes() + .Where(static type => type is { IsClass: true, IsAbstract: false, IsGenericTypeDefinition: false }) + .Where(static type => type + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Any(static method => method.GetCustomAttribute() is not null)); + + if (!nativeAvailable) + { + candidates = candidates.Where(static type => type != typeof(NativeComparisonBenchmarks)); + } + + return [.. candidates.OrderBy(static type => type.Name, StringComparer.Ordinal)]; + } + + /// + /// The horizontal rule drawn around the startup banner. + /// + private const string Rule = "================================================================================"; + + private static void PrintBanner(bool nativeAvailable) + { + Console.WriteLine(Rule); + Console.WriteLine(" TaLibStandard performance benchmarks"); + Console.WriteLine(Rule); + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + " Runtime : {0}", + RuntimeInformation.FrameworkDescription)); + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + " OS / arch : {0} / {1}", + RuntimeInformation.OSDescription.Trim(), + RuntimeInformation.OSArchitecture)); + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + " Server GC : {0} GC latency mode: {1} Logical cores: {2}", + GCSettings.IsServerGC, + GCSettings.LatencyMode, + Environment.ProcessorCount)); + Console.WriteLine(Rule); + + if (nativeAvailable) + { + Console.WriteLine(string.Format( + CultureInfo.InvariantCulture, + " NATIVE TA-LIB: AVAILABLE (resolved from '{0}')", + NativeTaLib.ResolvedName)); + Console.WriteLine(" NativeComparisonBenchmarks is included. Managed and native outputs are checked for"); + Console.WriteLine(" equivalence in [GlobalSetup] before anything is timed."); + } + else + { + Console.WriteLine(" NATIVE TA-LIB: NOT AVAILABLE - the managed-versus-C comparison is DISABLED."); + Console.WriteLine(string.Format(CultureInfo.InvariantCulture, " Probe result : {0}", NativeTaLib.Diagnostics)); + Console.WriteLine(" Everything else runs normally; the suite has no native dependency by design."); + Console.WriteLine(" To enable the comparison, install the TA-Lib C library and re-run:"); + Console.WriteLine(" macOS : brew install ta-lib"); + Console.WriteLine(" Debian : apt-get install libta-lib0 libta-lib-dev (or build from source)"); + Console.WriteLine(" Windows : put ta-lib.dll (or ta_libc_cdr.dll) on PATH or next to the executable"); + Console.WriteLine(" Any OS : set TALIB_NATIVE_LIBRARY to the full path of the shared library"); + } + + Console.WriteLine(Rule); + Console.WriteLine(); + } +} diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/README.md b/benchmarks/TechnicalAnalysis.Benchmarks/README.md new file mode 100644 index 00000000..000a432c --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/README.md @@ -0,0 +1,320 @@ +# TaLibStandard performance benchmarks + +A [BenchmarkDotNet](https://benchmarkdotnet.org) suite that measures the TaLibStandard indicator kernels and, +optionally, compares them head to head against the original TA-Lib C library. + +The suite is **fully offline and deterministic**. It never touches the network, never reads a market data provider +and needs no API key: all inputs come from a seeded synthetic OHLCV generator, so a run on your machine and a run on +CI see byte-identical data. + +Native TA-Lib is **entirely optional**. When it is not installed the suite prints a banner saying so and simply +drops the comparison benchmarks from the runnable set. + +--- + +## Quick start + +```bash +# from the repository root +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --list flat # see what exists +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --selfcheck # correctness gate, instant +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks # interactive menu +``` + +> Always run in `Release`. BenchmarkDotNet refuses to produce numbers from a `Debug` build, and rightly so. + +### Run everything + +```bash +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --filter '*' +``` + +This is a *long* run: 119 benchmark methods times three series lengths. Budget an hour or more. For day-to-day work, +filter. + +### Run one category + +Categories are `OverlapStudies`, `Momentum`, `VolatilityVolume`, `CandlePatterns`, `Precision`, `NativeComparison`, +plus the cross-cutting tags `TAFunc`, `TAMath`, `double`, `float` and `decimal`. + +```bash +# one suite +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --anyCategories Momentum + +# only the allocation-free kernels, across every suite +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --anyCategories TAFunc + +# the decimal candlestick benchmarks only (both tags must match) +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --allCategories CandlePatterns decimal +``` + +### Run one filter + +`--filter` takes glob patterns against the fully qualified method name. + +```bash +# every RSI benchmark anywhere in the suite +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --filter '*Rsi*' + +# one class +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --filter '*OverlapStudiesBenchmarks*' + +# one method, one length, quickly +dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- \ + --filter '*OverlapStudiesBenchmarks.Sma*' --job Short +``` + +### Useful switches + +Every standard BenchmarkDotNet argument is passed through. + +| Switch | Effect | +| --- | --- | +| `--list flat` / `--list tree` | Enumerate benchmarks without running them | +| `--filter ` | Select benchmarks by fully qualified name | +| `--anyCategories` / `--allCategories` | Select by `[BenchmarkCategory]` | +| `--job Dry` | One cold-start iteration. Smoke test only β€” **the timings are meaningless** | +| `--job Short` | Fewer iterations, roughly 5x faster, wider error bars | +| `--job Default` | The standard statistically rigorous job | +| `--runtimes net10.0` | Pick the target runtime explicitly | +| `--exporters github json` | Choose exporters at the command line | +| `--selfcheck` | Project-specific: invoke every benchmark once and assert it succeeds. No timing | + +Results are written to `BenchmarkDotNet.Artifacts/results/` next to the executable, as GitHub-flavoured markdown +(paste straight into an issue or release note) and as full JSON (machine readable, for tracking regressions between +releases). + +--- + +## What the suite measures + +### The two API paths, benchmarked separately + +Nearly every indicator appears **twice**, and the distinction is the single most important thing to understand when +reading the output. + +| Suffix | API | Output buffers | What the number means | +| --- | --- | --- | --- | +| `_TAFunc` | `TAFunc.Sma(..., ref outReal)` | Allocated once in `[GlobalSetup]`, i.e. **outside** the measured method | Pure algorithm cost. Allocation shows as ~0 B (a few bytes of boxing noise from the benchmark harness itself) | +| `_TAMath` | `TAMath.Sma(...) -> SmaResult` | Allocated **inside** the call, one array per output series plus one result record | Algorithm cost **plus** the price of the ergonomic API | + +The delta between the two is exactly what the convenience of `TAMath` costs, and the `Allocated` column quantifies +it: an `SmaResult` over 100 000 bars allocates ~800 KB per call while the `TAFunc` path allocates nothing. + +Some kernels allocate internally regardless of which entry point you use (`Sar`, `Natr`, `StochRsi`, `Ppo`, `Macd` +and the other composites build scratch arrays). The memory columns make that visible rather than hiding it. + +### The suites + +| Class | Indicators | Notes | +| --- | --- | --- | +| `OverlapStudiesBenchmarks` | Sma, Ema, Wma, Dema, Tema, Trima, Kama, T3, BollingerBands, MidPoint, Sar | Both API paths | +| `MomentumBenchmarks` | Rsi, Macd, Stoch, StochRsi, Adx, Cci, Mfi, WillR, Ppo, Roc, UltOsc, Aroon | Both API paths | +| `VolatilityVolumeBenchmarks` | Atr, Natr, TrueRange, Obv, Ad, AdOsc, StdDev, Variance, Correl, Beta | Both API paths. Correl and Beta consume a second, correlated instrument | +| `CandlePatternBenchmarks` | Doji, Engulfing, Hammer, HangingMan, Harami, Marubozu, SpinningTop, ShootingStar, 3WhiteSoldiers, 3BlackCrows, Piercing, HighWave, MorningStar | Each over `double`, `float` **and** `decimal`, to price the generic-math design | +| `PrecisionBenchmarks` | Sma, Ema, Rsi, Macd, BollingerBands, Atr, Correl | `double` vs `float` on the ergonomic API | +| `NativeComparisonBenchmarks` | Sma, Ema, Rsi, Macd, BBands, Atr, Adx, Stoch | Managed vs TA-Lib C. Only runs when the native library is present | + +Every class is a `[MemoryDiagnoser]` and parameterised over series length `1 000`, `10 000` and `100 000`. + +### What the precision benchmarks actually show + +`TAFunc` is written for `double` only. The `float` overloads on `TAMath` widen their inputs into freshly allocated +`double[]` arrays and then call the same kernel. A `float` benchmark therefore measures *the double kernel plus one +widening pass and one array allocation per input series*. It can never be faster than its `double` counterpart, and +`PrecisionBenchmarks` exists to put a number on the overhead rather than leave it to intuition. + +The candlestick suite is different: `TACandle` is generic over `T : IFloatingPoint`, so the JIT emits a dedicated +body per value type. `double` and `float` compile to hardware floating point; `decimal` falls back to the software +128-bit implementation. The three variants of each pattern price that choice directly. + +### The synthetic data + +`Data/MarketDataGenerator.cs` builds a discretised geometric Brownian motion, +`C[i] = C[i-1] * exp((mu - sigma^2/2) * dt + sigma * sqrt(dt) * Z)`, with `mu = 8%`, `sigma = 25%` and a trading-day +time step. On top of the close path it adds an overnight gap for the open and independent exponential wicks for the +high and low, so pattern recognisers see realistic bodies and shadows instead of degenerate bars. Volume is +log-normal and grows with the absolute return of the bar. A second, correlated instrument (rho = 0.65) feeds Correl +and Beta. + +Randomness comes from `DeterministicRandom` (xoshiro256\*\* seeded through SplitMix64), implemented in this project +rather than taken from `System.Random`, because the BCL does not guarantee that a seed produces the same sequence +across runtime versions. The default seed is `20240217`; change it only if every published number is regenerated at +the same time. + +Every price is rounded to four decimal places at generation time, which makes the `decimal` projection an **exact** +representation of the `double` projection. Only the `float` projection loses information. A precision comparison is +therefore a pure cost comparison, not a "different data" comparison. + +--- + +## Enabling the native TA-Lib comparison + +`NativeComparisonBenchmarks` runs the managed kernels against the original TA-Lib C library through P/Invoke. It is +skipped automatically when the library is missing, so nothing below is required to use the rest of the suite. + +### Install the native library + +**macOS** + +```bash +brew install ta-lib +# installs /opt/homebrew/lib/libta-lib.dylib on Apple silicon, /usr/local/lib/... on Intel +``` + +**Linux (Debian / Ubuntu)** + +```bash +# from the distribution, when packaged +sudo apt-get install libta-lib0 libta-lib-dev + +# or from source +curl -L -O https://github.com/TA-Lib/ta-lib/releases/download/v0.6.4/ta-lib-0.6.4-src.tar.gz +tar xzf ta-lib-0.6.4-src.tar.gz && cd ta-lib-0.6.4 +./configure --prefix=/usr/local && make && sudo make install && sudo ldconfig +``` + +**Windows** + +Install the official MSI, or download the prebuilt `ta-lib` archive, then make sure `ta-lib.dll` (older builds: +`ta_libc_cdr.dll`) is on `PATH` or sits next to the benchmark executable. + +### How discovery works + +`Interop/NativeTaLib.cs` registers a `DllImportResolver` and probes, in order: + +1. the path or name in the `TALIB_NATIVE_LIBRARY` environment variable, if set; +2. the plain names `ta-lib`, `ta_lib`, `libta-lib`, `libta_lib`, `ta_libc`, `ta_libc_cdr`, `libta-lib.so.0`, + `libta_lib.so.0`, letting the platform loader apply its own `lib` prefix and `.dylib` / `.so` / `.dll` suffix + conventions; +3. a list of absolute paths covering the Homebrew, MacPorts and autotools defaults. + +A candidate that loads but exports no `TA_Initialize` is rejected as "not TA-Lib". `TA_Initialize` is called once on +the first successful load and `TA_Shutdown` is registered on process exit. **The probe never throws**: on any +failure `NativeTaLib.IsAvailable` is simply `false` and `NativeTaLib.Diagnostics` explains why. + +If discovery fails for a library you know is installed: + +```bash +TALIB_NATIVE_LIBRARY=/opt/homebrew/lib/libta-lib.dylib \ + dotnet run -c Release --project benchmarks/TechnicalAnalysis.Benchmarks -- --anyCategories NativeComparison +``` + +### Correctness before speed + +A "faster" result that computes the wrong thing is worse than no result. `NativeComparisonBenchmarks.Setup()` runs +both implementations once and asserts, via `Interop/NativeEquivalence.cs`, that: + +* `outBegIdx` matches exactly (a shifted series would silently misalign every signal in time); +* `outNBElement` matches exactly; +* every produced value agrees within a relative tolerance of `1e-9`. + +A mismatch throws in `[GlobalSetup]`, which BenchmarkDotNet reports as a failed benchmark. No timing is ever +published for a pair that disagrees. + +Both sides use caller-supplied output buffers, so the comparison is algorithm against algorithm with no allocation +noise on either side. The managed side deliberately uses `TAFunc`, not `TAMath`, for that reason. The managed +implementation is the baseline of each group, so the `Ratio` column reads directly as *native time / managed time*. + +### Which C signatures are bound + +Bound and verified against the upstream `ta_func.h` / `ta_libc.h`: `TA_Initialize`, `TA_Shutdown`, `TA_SMA`, +`TA_EMA`, `TA_RSI`, `TA_MACD`, `TA_BBANDS`, `TA_ATR`, `TA_ADX`, `TA_STOCH`. All of them share the same shape β€” +`startIdx`, `endIdx`, the `const double[]` inputs, the `optIn*` parameters, `int *outBegIdx`, `int *outNBElement`, +then the `double[]` output buffers β€” return the C enum `TA_RetCode` (marshalled as `int`, `TA_SUCCESS == 0`) and use +the cdecl calling convention. The `TA_MAType` enum has the same ordinal order as `TechnicalAnalysis.Common.MAType` +(SMA, EMA, WMA, DEMA, TEMA, TRIMA, KAMA, MAMA, T3), so a plain cast is correct. + +Nothing else is bound. A wrong P/Invoke signature corrupts the stack and can produce plausible-looking but wrong +numbers, so the rule for this file is: bind only what is certain. + +--- + +## Reading the output + +``` +| Method | Categories | Length | Mean | Error | Op/s | Allocated | +|----------- |---------------------- |------- |-----------:|---------:|-------:|----------:| +| Sma_TAFunc | TAFunc,OverlapStudies | 100000 | xxx.x us | x.x us | xx,xxx | 0 B | +| Sma_TAMath | TAMath,OverlapStudies | 100000 | xxx.x us | x.x us | xx,xxx | 800,104 B| +``` + +* **Length** β€” the `[Params]` value: number of bars fed to the indicator. +* **Mean** β€” arithmetic mean per operation. One operation is one full pass over the whole series, not one bar. + Divide by `Length` for a per-bar figure. +* **Error** β€” half of the 99.9% confidence interval. If two means differ by less than the sum of their errors, treat + them as indistinguishable. +* **StdDev / Median** β€” appear when the distribution is noisy; a large `StdDev` relative to `Mean` means the + measurement is unstable and should not be quoted. +* **Op/s** β€” full-series operations per second, the reciprocal of `Mean`. +* **Ratio / Alloc Ratio** β€” versus the baseline of the same logical group. **Only `NativeComparisonBenchmarks` + declares baselines**, where `_Managed` is the baseline of each per-indicator group and the ratio reads as + *native Γ· managed*. The indicator suites declare none, because BenchmarkDotNet's default logical group is + (Job, Params): one baseline would ratio every row in the class against a single method, so `Ema_TAMath` would + be compared against `Sma_TAFunc` rather than against `Ema_TAFunc`. Compare the two rows of the same indicator + by hand instead. +* **Allocated** β€” managed bytes per operation, inclusive. This is the column that separates the two API paths. + +Scaling is the other thing worth reading. A well-behaved O(n) kernel should show `Mean` growing roughly 10x when +`Length` grows 10x. A super-linear jump between `10 000` and `100 000` usually means the working set stopped fitting +in cache, not that the algorithm changed. + +--- + +## Caveats + +Read these before quoting a number anywhere. + +* **`--job Dry` numbers are not measurements.** Dry runs a single cold-start iteration with no warmup, so it is + dominated by JIT compilation and first-touch page faults. It exists to prove the plumbing works. Use `--job Short` + at minimum and `--job Default` for anything you publish. +* **JIT warmup matters.** BenchmarkDotNet's default job includes a pilot phase, warmup iterations and overhead + subtraction precisely because the first few calls into a freshly JIT-compiled method are not representative. Do not + reduce the iteration counts to make a run finish faster and then quote the result. +* **ServerGC is on** (`true` in the csproj) with concurrent GC. + That is a deliberate choice matching a server-side analytics workload, and it changes the allocation-heavy + `_TAMath` numbers relative to a workstation-GC client. If your application runs workstation GC, re-run with + `--runtimes` and an appropriate job, or expect the ergonomic-path figures to differ. +* **Machine variance is real.** Absolute timings depend on CPU model, core count, memory bandwidth, thermal state, + power profile and what else is running. Laptops on battery throttle. CI runners are shared and noisy. Only compare + numbers produced on the same machine in the same session; treat cross-machine comparisons as qualitative. +* **Compare ratios, not absolutes, across time.** When tracking regressions between releases, the stable signal is + the ratio between two benchmarks measured together, not the microseconds. +* **The data is synthetic.** It is realistic enough to exercise the branches of every kernel and every candlestick + recogniser, but real market data has different volatility clustering and gap statistics. Branch-heavy indicators + such as the candlestick patterns may behave slightly differently on real series. +* **Native comparison is single-threaded, in-process P/Invoke.** The measured native time includes the managed to + native transition and the array pinning, which is exactly what a .NET consumer of the C library would pay, but it + is not the cost you would measure from a C program. +* **`--selfcheck` is a correctness gate, not a benchmark.** It invokes each method once at `Length = 1 000` and + asserts a successful `RetCode`; it deliberately reports no timing at all. + +--- + +## Project layout + +``` +benchmarks/TechnicalAnalysis.Benchmarks/ +β”œβ”€β”€ Benchmarks/ +β”‚ β”œβ”€β”€ BenchmarkCategories.cs category name constants +β”‚ β”œβ”€β”€ MarketDataBenchmarkBase.cs [Params] length, generated data, pre-allocated output buffers +β”‚ β”œβ”€β”€ OverlapStudiesBenchmarks.cs +β”‚ β”œβ”€β”€ MomentumBenchmarks.cs +β”‚ β”œβ”€β”€ VolatilityVolumeBenchmarks.cs +β”‚ β”œβ”€β”€ CandlePatternBenchmarks.cs +β”‚ β”œβ”€β”€ PrecisionBenchmarks.cs +β”‚ └── NativeComparisonBenchmarks.cs managed vs TA-Lib C, with equivalence assertion in [GlobalSetup] +β”œβ”€β”€ Configuration/ +β”‚ └── TaLibBenchmarkConfig.cs exporters, columns, ordering, summary style +β”œβ”€β”€ Data/ +β”‚ β”œβ”€β”€ DeterministicRandom.cs xoshiro256** + SplitMix64 + Box-Muller +β”‚ β”œβ”€β”€ MarketDataGenerator.cs seeded GBM OHLCV generator +β”‚ β”œβ”€β”€ MarketSeries.cs double / float / decimal projections + correlated reference series +β”‚ └── OhlcvSeries.cs +β”œβ”€β”€ Diagnostics/ +β”‚ └── BenchmarkSelfCheck.cs --selfcheck correctness gate +β”œβ”€β”€ Interop/ +β”‚ β”œβ”€β”€ NativeTaLib.cs opt-in P/Invoke bridge, never throws +β”‚ └── NativeEquivalence.cs managed vs native tolerance assertions +└── Program.cs banner, native detection, BenchmarkSwitcher +``` diff --git a/benchmarks/TechnicalAnalysis.Benchmarks/TechnicalAnalysis.Benchmarks.csproj b/benchmarks/TechnicalAnalysis.Benchmarks/TechnicalAnalysis.Benchmarks.csproj new file mode 100644 index 00000000..ec664a24 --- /dev/null +++ b/benchmarks/TechnicalAnalysis.Benchmarks/TechnicalAnalysis.Benchmarks.csproj @@ -0,0 +1,44 @@ + + + + Exe + false + true + TechnicalAnalysis.Benchmarks + + true + + true + true + true + + + + + $(NoWarn);CA1031;CA1051;CA1303;CA1307;CA1310;CA1707;CA1819;CA1812;CA5394;SYSLIB1054;IDE0032;IDE0290;IDE0350 + + + + + + + + + + + + diff --git a/docs/README.md b/docs/README.md index c61bc586..f72a343a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,23 +1,97 @@ # πŸ“„ TaLibStandard Documentation -## πŸ“š Read the documentation +Everything written about TaLibStandard lives here. There are three kinds of page, and it helps to know +which one you want: -### ...on GitHub (recommended) +| I want to… | Go to | +|------------|-------| +| Learn the library from scratch | [Guides](#-guides) β€” hand-written, worked examples, prose | +| Find an indicator and its signature | [Indicator catalog](#-indicator-catalog) β€” generated from the source, one table per category | +| Look up one type or member in detail | [API reference](#-api-reference) β€” generated from the XML docs | +| Never do `BegIdx` / `NBElement` arithmetic by hand | [Fluent API](guides/fluent-api.md) β€” `PriceSeries` in, bar-indexed `IndicatorSeries` out | -The main documentation for TaLibStandard is located in the following files. They are written in Markdown and can be read directly from GitHub: +--- -- **Candles**: [TechnicalAnalysis.Candles](https://github.com/phmatray/TaLibStandard/blob/main/docs/candles/Atypical.TechnicalAnalysis.Candles.md) -- **Functions**: [TechnicalAnalysis.Functions](https://github.com/phmatray/TaLibStandard/blob/main/docs/functions/Atypical.TechnicalAnalysis.Functions.md) -- **Common**: [TechnicalAnalysis.Common](https://github.com/phmatray/TaLibStandard/blob/main/docs/common/Atypical.TechnicalAnalysis.Common.md) +## πŸ“š Guides -### ...in your IDE +Hand-written and kept in `docs/guides/`. Start at the top. -If you are using an IDE that supports Markdown, you can open the following files directly in your IDE: +| Guide | What it covers | +|-------|----------------| +| [**Getting started**](guides/getting-started.md) | Installation, your first indicator, and the three things that trip everyone up: `RetCode`, `BegIdx`/`NBElement` output alignment, and the `double` / `float` / `decimal` story. Includes a hand-checkable worked SMA example, a lookback table, `TACore.Globals`, the low-level `TAFunc` API and thirteen common pitfalls. **Read this one first.** | +| [**Fluent API**](guides/fluent-api.md) | The bar-indexed layer over `TAMath`: `PriceSeries` factories, `IndicatorSeries` and its warm-up-is-`null` contract, crossings, `AsOf`, the nine shipped indicators with a worked example each, the `Align` escape hatch to the other 89, and error handling. | +| [**Indicator catalog**](indicators/README.md) | Every public entry point in one place β€” see below. | +| [**Real-time streaming**](guides/real-time-streaming.md) | Turning a tick feed into bars and indicators: architecture, both transports (SignalR hub and raw WebSocket), the message contracts, warm-up and null semantics, and production notes on Redis scale-out, backpressure, cancellation and when to abandon window-recompute for incremental state. | +| [**Backtesting**](guides/backtesting.md) | The engine model and its execution timeline, the structurally enforced no-look-ahead guarantee, the commission/slippage cost model, every performance metric with its formula and annualisation assumption, a complete worked `IStrategy`, CSV input, and an honest limitations section. | +| [**TradingView integration**](guides/tradingview-integration.md) | Mapping ~50 Pine `ta.*` functions to `TAMath`/`TACandle`, the three library defects that make some outputs impossible to reconcile at any tolerance, nine parity caveats (Wilder smoothing, EMA seeding, repainting, session alignment, dividend adjustment, …), UDF datafeed and Lightweight Charts wiring, and alert-webhook security. | +| [**Benchmarks**](guides/benchmarks.md) | What the BenchmarkDotNet suite measures and why, how to run one category or one filter, how to enable the native TA-Lib C comparison per platform, how to read every output column, the measured results, and the methodology caveats. | -- **Candles**: [./docs/candles/TechnicalAnalysis.Candles.md](./candles/Atypical.TechnicalAnalysis.Candles.md) -- **Functions**: [./docs/functions/TechnicalAnalysis.Functions.md](./functions/Atypical.TechnicalAnalysis.Functions.md) -- **Common**: [./docs/common/TechnicalAnalysis.Common.md](./common/Atypical.TechnicalAnalysis.Common.md) +--- -## 🀝 Contributing +## πŸ“‹ Indicator catalog -If you want to contribute to the documentation, just edit the summaries in the source code and build the solution. The documentation will be automatically generated in the `docs` folder. +[**docs/indicators/README.md**](indicators/README.md) lists every public entry point grouped into ten +categories, with its full signature, parameters and defaults, output property names, a one-line +description and a link to its generated API page. + +It is **generated** by [`tools/generate-indicator-catalog.py`](../tools/generate-indicator-catalog.py) +directly from `src/`. Do not hand-edit it. To regenerate: + +```shell +python3 tools/generate-indicator-catalog.py + +# or, to fail if the committed file is stale (suitable for CI) +python3 tools/generate-indicator-catalog.py --check +``` + +The generator exits non-zero β€” with a named list β€” if an indicator exists in the source but is missing +from its category table, or vice versa, so a newly added indicator cannot silently escape the catalog. + +--- + +## πŸ”Ž API reference + +Generated from the XML documentation comments by +[Doraku/DefaultDocumentation](https://github.com/Doraku/DefaultDocumentation) every time the solution is +built. One page per public type and member. + +### …on GitHub (recommended) + +- **Functions**: [Atypical.TechnicalAnalysis.Functions](https://github.com/phmatray/TaLibStandard/blob/main/docs/functions/Atypical.TechnicalAnalysis.Functions.md) +- **Candles**: [Atypical.TechnicalAnalysis.Candles](https://github.com/phmatray/TaLibStandard/blob/main/docs/candles/Atypical.TechnicalAnalysis.Candles.md) +- **Common**: [Atypical.TechnicalAnalysis.Common](https://github.com/phmatray/TaLibStandard/blob/main/docs/common/Atypical.TechnicalAnalysis.Common.md) + +### …in your IDE + +- **Functions**: [./functions/Atypical.TechnicalAnalysis.Functions.md](./functions/Atypical.TechnicalAnalysis.Functions.md) +- **Candles**: [./candles/Atypical.TechnicalAnalysis.Candles.md](./candles/Atypical.TechnicalAnalysis.Candles.md) +- **Common**: [./common/Atypical.TechnicalAnalysis.Common.md](./common/Atypical.TechnicalAnalysis.Common.md) + +There is also a flat [function list](./functions.md) if you only need the names. + +--- + +## πŸ§ͺ Runnable code + +The guides above describe these; the code itself is in the repository. + +| Project | Guide | +|---------|-------| +| [`samples/TechnicalAnalysis.Samples.RealTime`](../samples/TechnicalAnalysis.Samples.RealTime) + [`.Client`](../samples/TechnicalAnalysis.Samples.RealTime.Client) | [Real-time streaming](guides/real-time-streaming.md) | +| [`samples/TechnicalAnalysis.Samples.Backtesting`](../samples/TechnicalAnalysis.Samples.Backtesting) | [Backtesting](guides/backtesting.md) | +| [`benchmarks/TechnicalAnalysis.Benchmarks`](../benchmarks/TechnicalAnalysis.Benchmarks) | [Benchmarks](guides/benchmarks.md) | +| [`Demo.BlazorWasm`](../Demo.BlazorWasm) | β€” charts indicators in the browser | + +--- + +## 🀝 Contributing to the docs + +* **API reference** (`docs/functions/`, `docs/candles/`, `docs/common/`, `docs/links`): edit the XML + summaries in the source and build the solution. These folders are regenerated on every build; hand + edits are lost. +* **Indicator catalog** (`docs/indicators/README.md`): edit the `CATEGORIES` table in + `tools/generate-indicator-catalog.py`, then re-run the script. +* **Guides** (`docs/guides/*.md`): ordinary hand-written Markdown β€” edit directly. + +All summaries are written in English. If you would like to help translate the documentation, please open +an issue to discuss it. diff --git a/docs/candles/Candle2Crows_T_.md b/docs/candles/Candle2Crows_T_.md index ebc315de..fb216f9d 100644 --- a/docs/candles/Candle2Crows_T_.md +++ b/docs/candles/Candle2Crows_T_.md @@ -17,7 +17,7 @@ public class Candle2Crows : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle2Crows_T_.md#TechnicalAnalysis.Candles.Candle2Crows_T_.T 'TechnicalAnalysis\.Candles\.Candle2Crows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle2Crows\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle2Crows_T_.md#TechnicalAnalysis.Candles.Candle2Crows_T_.T 'TechnicalAnalysis\.Candles\.Candle2Crows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle2Crows\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/Candle3BlackCrows_T_.md b/docs/candles/Candle3BlackCrows_T_.md index 61989dc0..29cbf959 100644 --- a/docs/candles/Candle3BlackCrows_T_.md +++ b/docs/candles/Candle3BlackCrows_T_.md @@ -17,7 +17,7 @@ public class Candle3BlackCrows : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3BlackCrows_T_.md#TechnicalAnalysis.Candles.Candle3BlackCrows_T_.T 'TechnicalAnalysis\.Candles\.Candle3BlackCrows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle3BlackCrows\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3BlackCrows_T_.md#TechnicalAnalysis.Candles.Candle3BlackCrows_T_.T 'TechnicalAnalysis\.Candles\.Candle3BlackCrows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle3BlackCrows\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/Candle3Inside_T_.md b/docs/candles/Candle3Inside_T_.md index 47f43350..5ee64292 100644 --- a/docs/candles/Candle3Inside_T_.md +++ b/docs/candles/Candle3Inside_T_.md @@ -17,7 +17,7 @@ public class Candle3Inside : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3Inside_T_.md#TechnicalAnalysis.Candles.Candle3Inside_T_.T 'TechnicalAnalysis\.Candles\.Candle3Inside\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle3Inside\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3Inside_T_.md#TechnicalAnalysis.Candles.Candle3Inside_T_.T 'TechnicalAnalysis\.Candles\.Candle3Inside\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle3Inside\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/Candle3LineStrike_T_.md b/docs/candles/Candle3LineStrike_T_.md index 35b6d355..f6154a48 100644 --- a/docs/candles/Candle3LineStrike_T_.md +++ b/docs/candles/Candle3LineStrike_T_.md @@ -17,7 +17,7 @@ public class Candle3LineStrike : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3LineStrike_T_.md#TechnicalAnalysis.Candles.Candle3LineStrike_T_.T 'TechnicalAnalysis\.Candles\.Candle3LineStrike\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle3LineStrike\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3LineStrike_T_.md#TechnicalAnalysis.Candles.Candle3LineStrike_T_.T 'TechnicalAnalysis\.Candles\.Candle3LineStrike\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle3LineStrike\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/Candle3Outside_T_.md b/docs/candles/Candle3Outside_T_.md index 0f3a805f..d89517ec 100644 --- a/docs/candles/Candle3Outside_T_.md +++ b/docs/candles/Candle3Outside_T_.md @@ -17,7 +17,7 @@ public class Candle3Outside : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3Outside_T_.md#TechnicalAnalysis.Candles.Candle3Outside_T_.T 'TechnicalAnalysis\.Candles\.Candle3Outside\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle3Outside\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3Outside_T_.md#TechnicalAnalysis.Candles.Candle3Outside_T_.T 'TechnicalAnalysis\.Candles\.Candle3Outside\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle3Outside\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/Candle3StarsInSouth_T_.md b/docs/candles/Candle3StarsInSouth_T_.md index 16745681..12ac1c71 100644 --- a/docs/candles/Candle3StarsInSouth_T_.md +++ b/docs/candles/Candle3StarsInSouth_T_.md @@ -17,7 +17,7 @@ public class Candle3StarsInSouth : TechnicalAnalysis.Common.CandleIndicator\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle3StarsInSouth\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3StarsInSouth_T_.md#TechnicalAnalysis.Candles.Candle3StarsInSouth_T_.T 'TechnicalAnalysis\.Candles\.Candle3StarsInSouth\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle3StarsInSouth\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/Candle3WhiteSoldiers_T_.md b/docs/candles/Candle3WhiteSoldiers_T_.md index 1799f709..4dc59fcf 100644 --- a/docs/candles/Candle3WhiteSoldiers_T_.md +++ b/docs/candles/Candle3WhiteSoldiers_T_.md @@ -17,7 +17,7 @@ public class Candle3WhiteSoldiers : TechnicalAnalysis.Common.CandleIndicator< The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3WhiteSoldiers_T_.md#TechnicalAnalysis.Candles.Candle3WhiteSoldiers_T_.T 'TechnicalAnalysis\.Candles\.Candle3WhiteSoldiers\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 Candle3WhiteSoldiers\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](Candle3WhiteSoldiers_T_.md#TechnicalAnalysis.Candles.Candle3WhiteSoldiers_T_.T 'TechnicalAnalysis\.Candles\.Candle3WhiteSoldiers\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ Candle3WhiteSoldiers\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleAbandonedBaby_T_.md b/docs/candles/CandleAbandonedBaby_T_.md index 37295d53..be881890 100644 --- a/docs/candles/CandleAbandonedBaby_T_.md +++ b/docs/candles/CandleAbandonedBaby_T_.md @@ -17,7 +17,7 @@ public class CandleAbandonedBaby : TechnicalAnalysis.Common.CandleIndicator\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleAbandonedBaby\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleAbandonedBaby_T_.md#TechnicalAnalysis.Candles.CandleAbandonedBaby_T_.T 'TechnicalAnalysis\.Candles\.CandleAbandonedBaby\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleAbandonedBaby\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleAdvanceBlock_T_.md b/docs/candles/CandleAdvanceBlock_T_.md index 321c1499..b9d10b39 100644 --- a/docs/candles/CandleAdvanceBlock_T_.md +++ b/docs/candles/CandleAdvanceBlock_T_.md @@ -17,7 +17,7 @@ public class CandleAdvanceBlock : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleAdvanceBlock_T_.md#TechnicalAnalysis.Candles.CandleAdvanceBlock_T_.T 'TechnicalAnalysis\.Candles\.CandleAdvanceBlock\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleAdvanceBlock\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleAdvanceBlock_T_.md#TechnicalAnalysis.Candles.CandleAdvanceBlock_T_.T 'TechnicalAnalysis\.Candles\.CandleAdvanceBlock\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleAdvanceBlock\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleBeltHold_T_.md b/docs/candles/CandleBeltHold_T_.md index 69c27ae6..8788ebef 100644 --- a/docs/candles/CandleBeltHold_T_.md +++ b/docs/candles/CandleBeltHold_T_.md @@ -17,7 +17,7 @@ public class CandleBeltHold : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleBeltHold_T_.md#TechnicalAnalysis.Candles.CandleBeltHold_T_.T 'TechnicalAnalysis\.Candles\.CandleBeltHold\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleBeltHold\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleBeltHold_T_.md#TechnicalAnalysis.Candles.CandleBeltHold_T_.T 'TechnicalAnalysis\.Candles\.CandleBeltHold\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleBeltHold\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleBreakaway_T_.md b/docs/candles/CandleBreakaway_T_.md index d5973a77..a4cbad97 100644 --- a/docs/candles/CandleBreakaway_T_.md +++ b/docs/candles/CandleBreakaway_T_.md @@ -17,7 +17,7 @@ public class CandleBreakaway : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleBreakaway_T_.md#TechnicalAnalysis.Candles.CandleBreakaway_T_.T 'TechnicalAnalysis\.Candles\.CandleBreakaway\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleBreakaway\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleBreakaway_T_.md#TechnicalAnalysis.Candles.CandleBreakaway_T_.T 'TechnicalAnalysis\.Candles\.CandleBreakaway\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleBreakaway\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleClosingMarubozu_T_.md b/docs/candles/CandleClosingMarubozu_T_.md index 62df74ed..1595e82b 100644 --- a/docs/candles/CandleClosingMarubozu_T_.md +++ b/docs/candles/CandleClosingMarubozu_T_.md @@ -17,7 +17,7 @@ public class CandleClosingMarubozu : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleClosingMarubozu_T_.md#TechnicalAnalysis.Candles.CandleClosingMarubozu_T_.T 'TechnicalAnalysis\.Candles\.CandleClosingMarubozu\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleClosingMarubozu\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleClosingMarubozu_T_.md#TechnicalAnalysis.Candles.CandleClosingMarubozu_T_.T 'TechnicalAnalysis\.Candles\.CandleClosingMarubozu\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleClosingMarubozu\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleConcealBabySwallow_T_.md b/docs/candles/CandleConcealBabySwallow_T_.md index d726ea1b..078b0f0d 100644 --- a/docs/candles/CandleConcealBabySwallow_T_.md +++ b/docs/candles/CandleConcealBabySwallow_T_.md @@ -17,7 +17,7 @@ public class CandleConcealBabySwallow : TechnicalAnalysis.Common.CandleIndica The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleConcealBabySwallow_T_.md#TechnicalAnalysis.Candles.CandleConcealBabySwallow_T_.T 'TechnicalAnalysis\.Candles\.CandleConcealBabySwallow\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleConcealBabySwallow\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleConcealBabySwallow_T_.md#TechnicalAnalysis.Candles.CandleConcealBabySwallow_T_.T 'TechnicalAnalysis\.Candles\.CandleConcealBabySwallow\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleConcealBabySwallow\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleCounterAttack_T_.md b/docs/candles/CandleCounterAttack_T_.md index 1964454b..d7a532d5 100644 --- a/docs/candles/CandleCounterAttack_T_.md +++ b/docs/candles/CandleCounterAttack_T_.md @@ -17,7 +17,7 @@ public class CandleCounterAttack : TechnicalAnalysis.Common.CandleIndicator\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleCounterAttack\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleCounterAttack_T_.md#TechnicalAnalysis.Candles.CandleCounterAttack_T_.T 'TechnicalAnalysis\.Candles\.CandleCounterAttack\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleCounterAttack\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleDarkCloudCover_T_.md b/docs/candles/CandleDarkCloudCover_T_.md index b23aac6a..ebdb7153 100644 --- a/docs/candles/CandleDarkCloudCover_T_.md +++ b/docs/candles/CandleDarkCloudCover_T_.md @@ -17,7 +17,7 @@ public class CandleDarkCloudCover : TechnicalAnalysis.Common.CandleIndicator< The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDarkCloudCover_T_.md#TechnicalAnalysis.Candles.CandleDarkCloudCover_T_.T 'TechnicalAnalysis\.Candles\.CandleDarkCloudCover\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleDarkCloudCover\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDarkCloudCover_T_.md#TechnicalAnalysis.Candles.CandleDarkCloudCover_T_.T 'TechnicalAnalysis\.Candles\.CandleDarkCloudCover\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleDarkCloudCover\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleDojiStar_T_.md b/docs/candles/CandleDojiStar_T_.md index ea0a658f..95195138 100644 --- a/docs/candles/CandleDojiStar_T_.md +++ b/docs/candles/CandleDojiStar_T_.md @@ -17,7 +17,7 @@ public class CandleDojiStar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDojiStar_T_.md#TechnicalAnalysis.Candles.CandleDojiStar_T_.T 'TechnicalAnalysis\.Candles\.CandleDojiStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleDojiStar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDojiStar_T_.md#TechnicalAnalysis.Candles.CandleDojiStar_T_.T 'TechnicalAnalysis\.Candles\.CandleDojiStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleDojiStar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleDoji_T_.md b/docs/candles/CandleDoji_T_.md index f9b77d6a..81c1f7eb 100644 --- a/docs/candles/CandleDoji_T_.md +++ b/docs/candles/CandleDoji_T_.md @@ -17,7 +17,7 @@ public class CandleDoji : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDoji_T_.md#TechnicalAnalysis.Candles.CandleDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleDoji\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDoji_T_.md#TechnicalAnalysis.Candles.CandleDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleDoji\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleDragonflyDoji_T_.md b/docs/candles/CandleDragonflyDoji_T_.md index 15f63cbf..76cb24e4 100644 --- a/docs/candles/CandleDragonflyDoji_T_.md +++ b/docs/candles/CandleDragonflyDoji_T_.md @@ -17,7 +17,7 @@ public class CandleDragonflyDoji : TechnicalAnalysis.Common.CandleIndicator\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleDragonflyDoji\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleDragonflyDoji_T_.md#TechnicalAnalysis.Candles.CandleDragonflyDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleDragonflyDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleDragonflyDoji\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleEngulfing_T_.md b/docs/candles/CandleEngulfing_T_.md index d07c4083..eb1cd8bf 100644 --- a/docs/candles/CandleEngulfing_T_.md +++ b/docs/candles/CandleEngulfing_T_.md @@ -17,7 +17,7 @@ public class CandleEngulfing : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleEngulfing_T_.md#TechnicalAnalysis.Candles.CandleEngulfing_T_.T 'TechnicalAnalysis\.Candles\.CandleEngulfing\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleEngulfing\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleEngulfing_T_.md#TechnicalAnalysis.Candles.CandleEngulfing_T_.T 'TechnicalAnalysis\.Candles\.CandleEngulfing\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleEngulfing\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleEveningDojiStar_T_.md b/docs/candles/CandleEveningDojiStar_T_.md index 1d311a7b..b020ddc5 100644 --- a/docs/candles/CandleEveningDojiStar_T_.md +++ b/docs/candles/CandleEveningDojiStar_T_.md @@ -17,7 +17,7 @@ public class CandleEveningDojiStar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleEveningDojiStar_T_.md#TechnicalAnalysis.Candles.CandleEveningDojiStar_T_.T 'TechnicalAnalysis\.Candles\.CandleEveningDojiStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleEveningDojiStar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleEveningDojiStar_T_.md#TechnicalAnalysis.Candles.CandleEveningDojiStar_T_.T 'TechnicalAnalysis\.Candles\.CandleEveningDojiStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleEveningDojiStar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleEveningStar_T_.md b/docs/candles/CandleEveningStar_T_.md index c2407ebf..97ee6685 100644 --- a/docs/candles/CandleEveningStar_T_.md +++ b/docs/candles/CandleEveningStar_T_.md @@ -17,7 +17,7 @@ public class CandleEveningStar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleEveningStar_T_.md#TechnicalAnalysis.Candles.CandleEveningStar_T_.T 'TechnicalAnalysis\.Candles\.CandleEveningStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleEveningStar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleEveningStar_T_.md#TechnicalAnalysis.Candles.CandleEveningStar_T_.T 'TechnicalAnalysis\.Candles\.CandleEveningStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleEveningStar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleGapSideSideWhite_T_.md b/docs/candles/CandleGapSideSideWhite_T_.md index 6613a33b..2cf7d9f7 100644 --- a/docs/candles/CandleGapSideSideWhite_T_.md +++ b/docs/candles/CandleGapSideSideWhite_T_.md @@ -17,7 +17,7 @@ public class CandleGapSideSideWhite : TechnicalAnalysis.Common.CandleIndicato The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleGapSideSideWhite_T_.md#TechnicalAnalysis.Candles.CandleGapSideSideWhite_T_.T 'TechnicalAnalysis\.Candles\.CandleGapSideSideWhite\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleGapSideSideWhite\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleGapSideSideWhite_T_.md#TechnicalAnalysis.Candles.CandleGapSideSideWhite_T_.T 'TechnicalAnalysis\.Candles\.CandleGapSideSideWhite\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleGapSideSideWhite\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleGravestoneDoji_T_.md b/docs/candles/CandleGravestoneDoji_T_.md index 523f109b..620230ad 100644 --- a/docs/candles/CandleGravestoneDoji_T_.md +++ b/docs/candles/CandleGravestoneDoji_T_.md @@ -17,7 +17,7 @@ public class CandleGravestoneDoji : TechnicalAnalysis.Common.CandleIndicator< The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleGravestoneDoji_T_.md#TechnicalAnalysis.Candles.CandleGravestoneDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleGravestoneDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleGravestoneDoji\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleGravestoneDoji_T_.md#TechnicalAnalysis.Candles.CandleGravestoneDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleGravestoneDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleGravestoneDoji\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHammer_T_.md b/docs/candles/CandleHammer_T_.md index e95b848d..e5cb7340 100644 --- a/docs/candles/CandleHammer_T_.md +++ b/docs/candles/CandleHammer_T_.md @@ -17,7 +17,7 @@ public class CandleHammer : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHammer_T_.md#TechnicalAnalysis.Candles.CandleHammer_T_.T 'TechnicalAnalysis\.Candles\.CandleHammer\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHammer\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHammer_T_.md#TechnicalAnalysis.Candles.CandleHammer_T_.T 'TechnicalAnalysis\.Candles\.CandleHammer\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHammer\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHangingMan_T_.md b/docs/candles/CandleHangingMan_T_.md index 20b4ac4d..d22aa791 100644 --- a/docs/candles/CandleHangingMan_T_.md +++ b/docs/candles/CandleHangingMan_T_.md @@ -17,7 +17,7 @@ public class CandleHangingMan : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHangingMan_T_.md#TechnicalAnalysis.Candles.CandleHangingMan_T_.T 'TechnicalAnalysis\.Candles\.CandleHangingMan\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHangingMan\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHangingMan_T_.md#TechnicalAnalysis.Candles.CandleHangingMan_T_.T 'TechnicalAnalysis\.Candles\.CandleHangingMan\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHangingMan\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHaramiCross_T_.md b/docs/candles/CandleHaramiCross_T_.md index 8cf6c2ed..c4b6eaf7 100644 --- a/docs/candles/CandleHaramiCross_T_.md +++ b/docs/candles/CandleHaramiCross_T_.md @@ -17,7 +17,7 @@ public class CandleHaramiCross : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHaramiCross_T_.md#TechnicalAnalysis.Candles.CandleHaramiCross_T_.T 'TechnicalAnalysis\.Candles\.CandleHaramiCross\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHaramiCross\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHaramiCross_T_.md#TechnicalAnalysis.Candles.CandleHaramiCross_T_.T 'TechnicalAnalysis\.Candles\.CandleHaramiCross\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHaramiCross\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHarami_T_.md b/docs/candles/CandleHarami_T_.md index 7b491937..f7937982 100644 --- a/docs/candles/CandleHarami_T_.md +++ b/docs/candles/CandleHarami_T_.md @@ -17,7 +17,7 @@ public class CandleHarami : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHarami_T_.md#TechnicalAnalysis.Candles.CandleHarami_T_.T 'TechnicalAnalysis\.Candles\.CandleHarami\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHarami\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHarami_T_.md#TechnicalAnalysis.Candles.CandleHarami_T_.T 'TechnicalAnalysis\.Candles\.CandleHarami\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHarami\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHighWave_T_.md b/docs/candles/CandleHighWave_T_.md index 4ffde793..159056f7 100644 --- a/docs/candles/CandleHighWave_T_.md +++ b/docs/candles/CandleHighWave_T_.md @@ -17,7 +17,7 @@ public class CandleHighWave : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHighWave_T_.md#TechnicalAnalysis.Candles.CandleHighWave_T_.T 'TechnicalAnalysis\.Candles\.CandleHighWave\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHighWave\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHighWave_T_.md#TechnicalAnalysis.Candles.CandleHighWave_T_.T 'TechnicalAnalysis\.Candles\.CandleHighWave\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHighWave\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHikkakeMod_T_.md b/docs/candles/CandleHikkakeMod_T_.md index d0265a6d..a4e557fa 100644 --- a/docs/candles/CandleHikkakeMod_T_.md +++ b/docs/candles/CandleHikkakeMod_T_.md @@ -17,7 +17,7 @@ public class CandleHikkakeMod : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHikkakeMod_T_.md#TechnicalAnalysis.Candles.CandleHikkakeMod_T_.T 'TechnicalAnalysis\.Candles\.CandleHikkakeMod\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHikkakeMod\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHikkakeMod_T_.md#TechnicalAnalysis.Candles.CandleHikkakeMod_T_.T 'TechnicalAnalysis\.Candles\.CandleHikkakeMod\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHikkakeMod\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHikkake_T_.md b/docs/candles/CandleHikkake_T_.md index 4b256b89..de82588d 100644 --- a/docs/candles/CandleHikkake_T_.md +++ b/docs/candles/CandleHikkake_T_.md @@ -17,7 +17,7 @@ public class CandleHikkake : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHikkake_T_.md#TechnicalAnalysis.Candles.CandleHikkake_T_.T 'TechnicalAnalysis\.Candles\.CandleHikkake\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHikkake\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHikkake_T_.md#TechnicalAnalysis.Candles.CandleHikkake_T_.T 'TechnicalAnalysis\.Candles\.CandleHikkake\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHikkake\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleHomingPigeon_T_.md b/docs/candles/CandleHomingPigeon_T_.md index cc1b0564..5dd7d7e8 100644 --- a/docs/candles/CandleHomingPigeon_T_.md +++ b/docs/candles/CandleHomingPigeon_T_.md @@ -17,7 +17,7 @@ public class CandleHomingPigeon : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHomingPigeon_T_.md#TechnicalAnalysis.Candles.CandleHomingPigeon_T_.T 'TechnicalAnalysis\.Candles\.CandleHomingPigeon\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleHomingPigeon\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleHomingPigeon_T_.md#TechnicalAnalysis.Candles.CandleHomingPigeon_T_.T 'TechnicalAnalysis\.Candles\.CandleHomingPigeon\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleHomingPigeon\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleIdentical3Crows_T_.md b/docs/candles/CandleIdentical3Crows_T_.md index 4b82e5e5..e58fdd3f 100644 --- a/docs/candles/CandleIdentical3Crows_T_.md +++ b/docs/candles/CandleIdentical3Crows_T_.md @@ -17,7 +17,7 @@ public class CandleIdentical3Crows : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleIdentical3Crows_T_.md#TechnicalAnalysis.Candles.CandleIdentical3Crows_T_.T 'TechnicalAnalysis\.Candles\.CandleIdentical3Crows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleIdentical3Crows\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleIdentical3Crows_T_.md#TechnicalAnalysis.Candles.CandleIdentical3Crows_T_.T 'TechnicalAnalysis\.Candles\.CandleIdentical3Crows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleIdentical3Crows\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleInNeck_T_.md b/docs/candles/CandleInNeck_T_.md index 48336d11..94cd7eec 100644 --- a/docs/candles/CandleInNeck_T_.md +++ b/docs/candles/CandleInNeck_T_.md @@ -17,7 +17,7 @@ public class CandleInNeck : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleInNeck_T_.md#TechnicalAnalysis.Candles.CandleInNeck_T_.T 'TechnicalAnalysis\.Candles\.CandleInNeck\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleInNeck\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleInNeck_T_.md#TechnicalAnalysis.Candles.CandleInNeck_T_.T 'TechnicalAnalysis\.Candles\.CandleInNeck\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleInNeck\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleInvertedHammer_T_.md b/docs/candles/CandleInvertedHammer_T_.md index 5ec48f09..4595c9fb 100644 --- a/docs/candles/CandleInvertedHammer_T_.md +++ b/docs/candles/CandleInvertedHammer_T_.md @@ -17,7 +17,7 @@ public class CandleInvertedHammer : TechnicalAnalysis.Common.CandleIndicator< The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleInvertedHammer_T_.md#TechnicalAnalysis.Candles.CandleInvertedHammer_T_.T 'TechnicalAnalysis\.Candles\.CandleInvertedHammer\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleInvertedHammer\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleInvertedHammer_T_.md#TechnicalAnalysis.Candles.CandleInvertedHammer_T_.T 'TechnicalAnalysis\.Candles\.CandleInvertedHammer\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleInvertedHammer\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleKickingByLength_T_.md b/docs/candles/CandleKickingByLength_T_.md index e09f4890..867cafa0 100644 --- a/docs/candles/CandleKickingByLength_T_.md +++ b/docs/candles/CandleKickingByLength_T_.md @@ -17,7 +17,7 @@ public class CandleKickingByLength : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleKickingByLength_T_.md#TechnicalAnalysis.Candles.CandleKickingByLength_T_.T 'TechnicalAnalysis\.Candles\.CandleKickingByLength\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleKickingByLength\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleKickingByLength_T_.md#TechnicalAnalysis.Candles.CandleKickingByLength_T_.T 'TechnicalAnalysis\.Candles\.CandleKickingByLength\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleKickingByLength\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleKicking_T_.md b/docs/candles/CandleKicking_T_.md index eaa1de05..35ed4765 100644 --- a/docs/candles/CandleKicking_T_.md +++ b/docs/candles/CandleKicking_T_.md @@ -17,7 +17,7 @@ public class CandleKicking : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleKicking_T_.md#TechnicalAnalysis.Candles.CandleKicking_T_.T 'TechnicalAnalysis\.Candles\.CandleKicking\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleKicking\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleKicking_T_.md#TechnicalAnalysis.Candles.CandleKicking_T_.T 'TechnicalAnalysis\.Candles\.CandleKicking\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleKicking\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleLadderBottom_T_.md b/docs/candles/CandleLadderBottom_T_.md index c97f4575..badaab0a 100644 --- a/docs/candles/CandleLadderBottom_T_.md +++ b/docs/candles/CandleLadderBottom_T_.md @@ -17,7 +17,7 @@ public class CandleLadderBottom : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleLadderBottom_T_.md#TechnicalAnalysis.Candles.CandleLadderBottom_T_.T 'TechnicalAnalysis\.Candles\.CandleLadderBottom\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleLadderBottom\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleLadderBottom_T_.md#TechnicalAnalysis.Candles.CandleLadderBottom_T_.T 'TechnicalAnalysis\.Candles\.CandleLadderBottom\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleLadderBottom\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleLongLeggedDoji_T_.md b/docs/candles/CandleLongLeggedDoji_T_.md index 3d492e4c..45ecaf7b 100644 --- a/docs/candles/CandleLongLeggedDoji_T_.md +++ b/docs/candles/CandleLongLeggedDoji_T_.md @@ -17,7 +17,7 @@ public class CandleLongLeggedDoji : TechnicalAnalysis.Common.CandleIndicator< The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleLongLeggedDoji_T_.md#TechnicalAnalysis.Candles.CandleLongLeggedDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleLongLeggedDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleLongLeggedDoji\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleLongLeggedDoji_T_.md#TechnicalAnalysis.Candles.CandleLongLeggedDoji_T_.T 'TechnicalAnalysis\.Candles\.CandleLongLeggedDoji\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleLongLeggedDoji\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleLongLine_T_.md b/docs/candles/CandleLongLine_T_.md index 8c897284..00b4bad4 100644 --- a/docs/candles/CandleLongLine_T_.md +++ b/docs/candles/CandleLongLine_T_.md @@ -17,7 +17,7 @@ public class CandleLongLine : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleLongLine_T_.md#TechnicalAnalysis.Candles.CandleLongLine_T_.T 'TechnicalAnalysis\.Candles\.CandleLongLine\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleLongLine\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleLongLine_T_.md#TechnicalAnalysis.Candles.CandleLongLine_T_.T 'TechnicalAnalysis\.Candles\.CandleLongLine\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleLongLine\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleMarubozu_T_.md b/docs/candles/CandleMarubozu_T_.md index 889c40c1..b64fbdb0 100644 --- a/docs/candles/CandleMarubozu_T_.md +++ b/docs/candles/CandleMarubozu_T_.md @@ -17,7 +17,7 @@ public class CandleMarubozu : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMarubozu_T_.md#TechnicalAnalysis.Candles.CandleMarubozu_T_.T 'TechnicalAnalysis\.Candles\.CandleMarubozu\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleMarubozu\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMarubozu_T_.md#TechnicalAnalysis.Candles.CandleMarubozu_T_.T 'TechnicalAnalysis\.Candles\.CandleMarubozu\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleMarubozu\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleMatHold_T_.md b/docs/candles/CandleMatHold_T_.md index c64d0377..6c72f244 100644 --- a/docs/candles/CandleMatHold_T_.md +++ b/docs/candles/CandleMatHold_T_.md @@ -17,7 +17,7 @@ public class CandleMatHold : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMatHold_T_.md#TechnicalAnalysis.Candles.CandleMatHold_T_.T 'TechnicalAnalysis\.Candles\.CandleMatHold\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleMatHold\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMatHold_T_.md#TechnicalAnalysis.Candles.CandleMatHold_T_.T 'TechnicalAnalysis\.Candles\.CandleMatHold\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleMatHold\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleMatchingLow_T_.md b/docs/candles/CandleMatchingLow_T_.md index 58c00fa1..beec6b51 100644 --- a/docs/candles/CandleMatchingLow_T_.md +++ b/docs/candles/CandleMatchingLow_T_.md @@ -17,7 +17,7 @@ public class CandleMatchingLow : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMatchingLow_T_.md#TechnicalAnalysis.Candles.CandleMatchingLow_T_.T 'TechnicalAnalysis\.Candles\.CandleMatchingLow\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleMatchingLow\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMatchingLow_T_.md#TechnicalAnalysis.Candles.CandleMatchingLow_T_.T 'TechnicalAnalysis\.Candles\.CandleMatchingLow\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleMatchingLow\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleMorningDojiStar_T_.md b/docs/candles/CandleMorningDojiStar_T_.md index 6a0b328d..bb6c0518 100644 --- a/docs/candles/CandleMorningDojiStar_T_.md +++ b/docs/candles/CandleMorningDojiStar_T_.md @@ -17,7 +17,7 @@ public class CandleMorningDojiStar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMorningDojiStar_T_.md#TechnicalAnalysis.Candles.CandleMorningDojiStar_T_.T 'TechnicalAnalysis\.Candles\.CandleMorningDojiStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleMorningDojiStar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMorningDojiStar_T_.md#TechnicalAnalysis.Candles.CandleMorningDojiStar_T_.T 'TechnicalAnalysis\.Candles\.CandleMorningDojiStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleMorningDojiStar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleMorningStar_T_.md b/docs/candles/CandleMorningStar_T_.md index 1b913486..816e3b74 100644 --- a/docs/candles/CandleMorningStar_T_.md +++ b/docs/candles/CandleMorningStar_T_.md @@ -17,7 +17,7 @@ public class CandleMorningStar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMorningStar_T_.md#TechnicalAnalysis.Candles.CandleMorningStar_T_.T 'TechnicalAnalysis\.Candles\.CandleMorningStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleMorningStar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleMorningStar_T_.md#TechnicalAnalysis.Candles.CandleMorningStar_T_.T 'TechnicalAnalysis\.Candles\.CandleMorningStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleMorningStar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleOnNeck_T_.md b/docs/candles/CandleOnNeck_T_.md index 1476612d..8b1c293b 100644 --- a/docs/candles/CandleOnNeck_T_.md +++ b/docs/candles/CandleOnNeck_T_.md @@ -17,7 +17,7 @@ public class CandleOnNeck : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleOnNeck_T_.md#TechnicalAnalysis.Candles.CandleOnNeck_T_.T 'TechnicalAnalysis\.Candles\.CandleOnNeck\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleOnNeck\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleOnNeck_T_.md#TechnicalAnalysis.Candles.CandleOnNeck_T_.T 'TechnicalAnalysis\.Candles\.CandleOnNeck\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleOnNeck\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandlePiercing_T_.md b/docs/candles/CandlePiercing_T_.md index a7367c90..b31845ae 100644 --- a/docs/candles/CandlePiercing_T_.md +++ b/docs/candles/CandlePiercing_T_.md @@ -17,7 +17,7 @@ public class CandlePiercing : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandlePiercing_T_.md#TechnicalAnalysis.Candles.CandlePiercing_T_.T 'TechnicalAnalysis\.Candles\.CandlePiercing\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandlePiercing\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandlePiercing_T_.md#TechnicalAnalysis.Candles.CandlePiercing_T_.T 'TechnicalAnalysis\.Candles\.CandlePiercing\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandlePiercing\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleRickshawMan_T_.md b/docs/candles/CandleRickshawMan_T_.md index f6206fd7..ba478a1e 100644 --- a/docs/candles/CandleRickshawMan_T_.md +++ b/docs/candles/CandleRickshawMan_T_.md @@ -17,7 +17,7 @@ public class CandleRickshawMan : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleRickshawMan_T_.md#TechnicalAnalysis.Candles.CandleRickshawMan_T_.T 'TechnicalAnalysis\.Candles\.CandleRickshawMan\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleRickshawMan\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleRickshawMan_T_.md#TechnicalAnalysis.Candles.CandleRickshawMan_T_.T 'TechnicalAnalysis\.Candles\.CandleRickshawMan\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleRickshawMan\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleRiseFall3Methods_T_.md b/docs/candles/CandleRiseFall3Methods_T_.md index ef569658..9aa131fc 100644 --- a/docs/candles/CandleRiseFall3Methods_T_.md +++ b/docs/candles/CandleRiseFall3Methods_T_.md @@ -17,7 +17,7 @@ public class CandleRiseFall3Methods : TechnicalAnalysis.Common.CandleIndicato The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleRiseFall3Methods_T_.md#TechnicalAnalysis.Candles.CandleRiseFall3Methods_T_.T 'TechnicalAnalysis\.Candles\.CandleRiseFall3Methods\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleRiseFall3Methods\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleRiseFall3Methods_T_.md#TechnicalAnalysis.Candles.CandleRiseFall3Methods_T_.T 'TechnicalAnalysis\.Candles\.CandleRiseFall3Methods\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleRiseFall3Methods\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleSeparatingLines_T_.md b/docs/candles/CandleSeparatingLines_T_.md index 066d0370..382fa2d5 100644 --- a/docs/candles/CandleSeparatingLines_T_.md +++ b/docs/candles/CandleSeparatingLines_T_.md @@ -17,7 +17,7 @@ public class CandleSeparatingLines : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleSeparatingLines_T_.md#TechnicalAnalysis.Candles.CandleSeparatingLines_T_.T 'TechnicalAnalysis\.Candles\.CandleSeparatingLines\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleSeparatingLines\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleSeparatingLines_T_.md#TechnicalAnalysis.Candles.CandleSeparatingLines_T_.T 'TechnicalAnalysis\.Candles\.CandleSeparatingLines\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleSeparatingLines\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleShootingStar_T_.md b/docs/candles/CandleShootingStar_T_.md index a23d0299..212c3e56 100644 --- a/docs/candles/CandleShootingStar_T_.md +++ b/docs/candles/CandleShootingStar_T_.md @@ -17,7 +17,7 @@ public class CandleShootingStar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleShootingStar_T_.md#TechnicalAnalysis.Candles.CandleShootingStar_T_.T 'TechnicalAnalysis\.Candles\.CandleShootingStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleShootingStar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleShootingStar_T_.md#TechnicalAnalysis.Candles.CandleShootingStar_T_.T 'TechnicalAnalysis\.Candles\.CandleShootingStar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleShootingStar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleShortLine_T_.md b/docs/candles/CandleShortLine_T_.md index 25849049..63b8ab52 100644 --- a/docs/candles/CandleShortLine_T_.md +++ b/docs/candles/CandleShortLine_T_.md @@ -17,7 +17,7 @@ public class CandleShortLine : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleShortLine_T_.md#TechnicalAnalysis.Candles.CandleShortLine_T_.T 'TechnicalAnalysis\.Candles\.CandleShortLine\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleShortLine\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleShortLine_T_.md#TechnicalAnalysis.Candles.CandleShortLine_T_.T 'TechnicalAnalysis\.Candles\.CandleShortLine\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleShortLine\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleSpinningTop_T_.md b/docs/candles/CandleSpinningTop_T_.md index d220bd4f..4e5b94fd 100644 --- a/docs/candles/CandleSpinningTop_T_.md +++ b/docs/candles/CandleSpinningTop_T_.md @@ -17,7 +17,7 @@ public class CandleSpinningTop : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleSpinningTop_T_.md#TechnicalAnalysis.Candles.CandleSpinningTop_T_.T 'TechnicalAnalysis\.Candles\.CandleSpinningTop\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleSpinningTop\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleSpinningTop_T_.md#TechnicalAnalysis.Candles.CandleSpinningTop_T_.T 'TechnicalAnalysis\.Candles\.CandleSpinningTop\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleSpinningTop\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleStalledPattern_T_.md b/docs/candles/CandleStalledPattern_T_.md index d6404efa..0eff1e67 100644 --- a/docs/candles/CandleStalledPattern_T_.md +++ b/docs/candles/CandleStalledPattern_T_.md @@ -17,7 +17,7 @@ public class CandleStalledPattern : TechnicalAnalysis.Common.CandleIndicator< The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleStalledPattern_T_.md#TechnicalAnalysis.Candles.CandleStalledPattern_T_.T 'TechnicalAnalysis\.Candles\.CandleStalledPattern\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleStalledPattern\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleStalledPattern_T_.md#TechnicalAnalysis.Candles.CandleStalledPattern_T_.T 'TechnicalAnalysis\.Candles\.CandleStalledPattern\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleStalledPattern\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleStickSandwich_T_.md b/docs/candles/CandleStickSandwich_T_.md index 580f30a8..68578e57 100644 --- a/docs/candles/CandleStickSandwich_T_.md +++ b/docs/candles/CandleStickSandwich_T_.md @@ -17,7 +17,7 @@ public class CandleStickSandwich : TechnicalAnalysis.Common.CandleIndicator\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleStickSandwich\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleStickSandwich_T_.md#TechnicalAnalysis.Candles.CandleStickSandwich_T_.T 'TechnicalAnalysis\.Candles\.CandleStickSandwich\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleStickSandwich\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleTakuri_T_.md b/docs/candles/CandleTakuri_T_.md index 4feb02d5..d39dd04f 100644 --- a/docs/candles/CandleTakuri_T_.md +++ b/docs/candles/CandleTakuri_T_.md @@ -17,7 +17,7 @@ public class CandleTakuri : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleTakuri_T_.md#TechnicalAnalysis.Candles.CandleTakuri_T_.T 'TechnicalAnalysis\.Candles\.CandleTakuri\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleTakuri\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleTakuri_T_.md#TechnicalAnalysis.Candles.CandleTakuri_T_.T 'TechnicalAnalysis\.Candles\.CandleTakuri\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleTakuri\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleTasukiGap_T_.md b/docs/candles/CandleTasukiGap_T_.md index 68dc3141..d79bf21d 100644 --- a/docs/candles/CandleTasukiGap_T_.md +++ b/docs/candles/CandleTasukiGap_T_.md @@ -17,7 +17,7 @@ public class CandleTasukiGap : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleTasukiGap_T_.md#TechnicalAnalysis.Candles.CandleTasukiGap_T_.T 'TechnicalAnalysis\.Candles\.CandleTasukiGap\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleTasukiGap\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleTasukiGap_T_.md#TechnicalAnalysis.Candles.CandleTasukiGap_T_.T 'TechnicalAnalysis\.Candles\.CandleTasukiGap\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleTasukiGap\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleThrusting_T_.md b/docs/candles/CandleThrusting_T_.md index 33abac9d..da963f9a 100644 --- a/docs/candles/CandleThrusting_T_.md +++ b/docs/candles/CandleThrusting_T_.md @@ -17,7 +17,7 @@ public class CandleThrusting : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleThrusting_T_.md#TechnicalAnalysis.Candles.CandleThrusting_T_.T 'TechnicalAnalysis\.Candles\.CandleThrusting\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleThrusting\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleThrusting_T_.md#TechnicalAnalysis.Candles.CandleThrusting_T_.T 'TechnicalAnalysis\.Candles\.CandleThrusting\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleThrusting\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleTristar_T_.md b/docs/candles/CandleTristar_T_.md index 22c25491..5a30b36e 100644 --- a/docs/candles/CandleTristar_T_.md +++ b/docs/candles/CandleTristar_T_.md @@ -17,7 +17,7 @@ public class CandleTristar : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleTristar_T_.md#TechnicalAnalysis.Candles.CandleTristar_T_.T 'TechnicalAnalysis\.Candles\.CandleTristar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleTristar\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleTristar_T_.md#TechnicalAnalysis.Candles.CandleTristar_T_.T 'TechnicalAnalysis\.Candles\.CandleTristar\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleTristar\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleUnique3River_T_.md b/docs/candles/CandleUnique3River_T_.md index 1b45005d..07814cab 100644 --- a/docs/candles/CandleUnique3River_T_.md +++ b/docs/candles/CandleUnique3River_T_.md @@ -17,7 +17,7 @@ public class CandleUnique3River : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleUnique3River_T_.md#TechnicalAnalysis.Candles.CandleUnique3River_T_.T 'TechnicalAnalysis\.Candles\.CandleUnique3River\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleUnique3River\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleUnique3River_T_.md#TechnicalAnalysis.Candles.CandleUnique3River_T_.T 'TechnicalAnalysis\.Candles\.CandleUnique3River\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleUnique3River\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleUpsideGap2Crows_T_.md b/docs/candles/CandleUpsideGap2Crows_T_.md index 237f9191..ee445533 100644 --- a/docs/candles/CandleUpsideGap2Crows_T_.md +++ b/docs/candles/CandleUpsideGap2Crows_T_.md @@ -17,7 +17,7 @@ public class CandleUpsideGap2Crows : TechnicalAnalysis.Common.CandleIndicator The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleUpsideGap2Crows_T_.md#TechnicalAnalysis.Candles.CandleUpsideGap2Crows_T_.T 'TechnicalAnalysis\.Candles\.CandleUpsideGap2Crows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleUpsideGap2Crows\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleUpsideGap2Crows_T_.md#TechnicalAnalysis.Candles.CandleUpsideGap2Crows_T_.T 'TechnicalAnalysis\.Candles\.CandleUpsideGap2Crows\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleUpsideGap2Crows\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/CandleXSideGap3Methods_T_.md b/docs/candles/CandleXSideGap3Methods_T_.md index a98ae6bd..6c0f307d 100644 --- a/docs/candles/CandleXSideGap3Methods_T_.md +++ b/docs/candles/CandleXSideGap3Methods_T_.md @@ -17,7 +17,7 @@ public class CandleXSideGap3Methods : TechnicalAnalysis.Common.CandleIndicato The type of the array elements\. -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleXSideGap3Methods_T_.md#TechnicalAnalysis.Candles.CandleXSideGap3Methods_T_.T 'TechnicalAnalysis\.Candles\.CandleXSideGap3Methods\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') 🡒 CandleXSideGap3Methods\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.CandleIndicator<](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1')[T](CandleXSideGap3Methods_T_.md#TechnicalAnalysis.Candles.CandleXSideGap3Methods_T_.T 'TechnicalAnalysis\.Candles\.CandleXSideGap3Methods\\.T')[>](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.candleindicator-1 'TechnicalAnalysis\.Common\.CandleIndicator\`1') β†’ CandleXSideGap3Methods\ | Constructors | | | :--- | :--- | diff --git a/docs/candles/TACandle.md b/docs/candles/TACandle.md index 3658aa57..f24b623e 100644 --- a/docs/candles/TACandle.md +++ b/docs/candles/TACandle.md @@ -7,7 +7,7 @@ public static class TACandle ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 TACandle +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ TACandle | Methods | | | :--- | :--- | diff --git a/docs/common/ArrayExtension.md b/docs/common/ArrayExtension.md index 7759fa53..ef12b660 100644 --- a/docs/common/ArrayExtension.md +++ b/docs/common/ArrayExtension.md @@ -9,7 +9,7 @@ Provides a set of extension methods for arrays\. public static class ArrayExtension ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 ArrayExtension +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ ArrayExtension | Methods | | | :--- | :--- | diff --git a/docs/common/CandleIndicatorResult.md b/docs/common/CandleIndicatorResult.md index ad130cc9..5c4064c8 100644 --- a/docs/common/CandleIndicatorResult.md +++ b/docs/common/CandleIndicatorResult.md @@ -9,7 +9,7 @@ Represents the result of the candlestick pattern indicator\. public record CandleIndicatorResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 CandleIndicatorResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ CandleIndicatorResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CandleIndicatorResult](CandleIndicatorResult.md 'TechnicalAnalysis\.Common\.CandleIndicatorResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/common/CandleIndicator_T_.md b/docs/common/CandleIndicator_T_.md index a6c797d7..5625b57e 100644 --- a/docs/common/CandleIndicator_T_.md +++ b/docs/common/CandleIndicator_T_.md @@ -15,7 +15,7 @@ public abstract class CandleIndicator `T` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 CandleIndicator\ +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ CandleIndicator\ | Constructors | | | :--- | :--- | diff --git a/docs/common/CandleSetting.md b/docs/common/CandleSetting.md index 7cf9aede..2be031a7 100644 --- a/docs/common/CandleSetting.md +++ b/docs/common/CandleSetting.md @@ -9,7 +9,7 @@ Represents a setting for a candlestick pattern in technical analysis\. public sealed class CandleSetting ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 CandleSetting +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ CandleSetting | Constructors | | | :--- | :--- | diff --git a/docs/common/DualOutputResult.md b/docs/common/DualOutputResult.md index e972040d..d451c7cb 100644 --- a/docs/common/DualOutputResult.md +++ b/docs/common/DualOutputResult.md @@ -9,7 +9,7 @@ Base class for indicator results that produce two output arrays\. public abstract record DualOutputResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 DualOutputResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ DualOutputResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[DualOutputResult](DualOutputResult.md 'TechnicalAnalysis\.Common\.DualOutputResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/common/GlobalsType.md b/docs/common/GlobalsType.md index 8ad2206e..9acdfb4e 100644 --- a/docs/common/GlobalsType.md +++ b/docs/common/GlobalsType.md @@ -9,7 +9,7 @@ Represents the global settings for the Technical Analysis library\. public sealed class GlobalsType ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 GlobalsType +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ GlobalsType | Constructors | | | :--- | :--- | diff --git a/docs/common/IndicatorResult.md b/docs/common/IndicatorResult.md index 5ac53d82..86559df9 100644 --- a/docs/common/IndicatorResult.md +++ b/docs/common/IndicatorResult.md @@ -9,13 +9,13 @@ Represents an abstract base class for technical indicators\. public abstract record IndicatorResult : System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 IndicatorResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ IndicatorResult Derived -↳ [CandleIndicatorResult](CandleIndicatorResult.md 'TechnicalAnalysis\.Common\.CandleIndicatorResult') -↳ [DualOutputResult](DualOutputResult.md 'TechnicalAnalysis\.Common\.DualOutputResult') -↳ [SingleOutputResult](SingleOutputResult.md 'TechnicalAnalysis\.Common\.SingleOutputResult') -↳ [TripleOutputResult](TripleOutputResult.md 'TechnicalAnalysis\.Common\.TripleOutputResult') +↳ [CandleIndicatorResult](CandleIndicatorResult.md 'TechnicalAnalysis\.Common\.CandleIndicatorResult') +↳ [DualOutputResult](DualOutputResult.md 'TechnicalAnalysis\.Common\.DualOutputResult') +↳ [SingleOutputResult](SingleOutputResult.md 'TechnicalAnalysis\.Common\.SingleOutputResult') +↳ [TripleOutputResult](TripleOutputResult.md 'TechnicalAnalysis\.Common\.TripleOutputResult') Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/common/MathHelper.md b/docs/common/MathHelper.md index 2593a0d8..5238bafd 100644 --- a/docs/common/MathHelper.md +++ b/docs/common/MathHelper.md @@ -9,7 +9,7 @@ Provides helper methods for mathematical function indicators\. public static class MathHelper ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 MathHelper +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ MathHelper ### Remarks This helper class consolidates the common boilerplate code used by simple diff --git a/docs/common/MoneyFlow.md b/docs/common/MoneyFlow.md index b6c11d53..a9fcdaf0 100644 --- a/docs/common/MoneyFlow.md +++ b/docs/common/MoneyFlow.md @@ -7,7 +7,7 @@ public class MoneyFlow ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 MoneyFlow +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ MoneyFlow | Properties | | | :--- | :--- | diff --git a/docs/common/SingleOutputResult.md b/docs/common/SingleOutputResult.md index 53813566..870a468b 100644 --- a/docs/common/SingleOutputResult.md +++ b/docs/common/SingleOutputResult.md @@ -9,7 +9,7 @@ Base class for indicator results that produce a single output array\. public abstract record SingleOutputResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 SingleOutputResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ SingleOutputResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SingleOutputResult](SingleOutputResult.md 'TechnicalAnalysis\.Common\.SingleOutputResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/common/TACore.md b/docs/common/TACore.md index 0ac116f2..e6b01581 100644 --- a/docs/common/TACore.md +++ b/docs/common/TACore.md @@ -9,7 +9,7 @@ Provides core functionalities for the Technical Analysis library\. public static class TACore ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 TACore +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ TACore | Properties | | | :--- | :--- | diff --git a/docs/common/TripleOutputResult.md b/docs/common/TripleOutputResult.md index 1c273262..ca32f3af 100644 --- a/docs/common/TripleOutputResult.md +++ b/docs/common/TripleOutputResult.md @@ -9,7 +9,7 @@ Base class for indicator results that produce three output arrays\. public abstract record TripleOutputResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 TripleOutputResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [IndicatorResult](IndicatorResult.md 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ TripleOutputResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TripleOutputResult](TripleOutputResult.md 'TechnicalAnalysis\.Common\.TripleOutputResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/common/ValidationHelper.md b/docs/common/ValidationHelper.md index 33e8e4da..f4764531 100644 --- a/docs/common/ValidationHelper.md +++ b/docs/common/ValidationHelper.md @@ -9,7 +9,7 @@ Provides centralized validation methods for technical analysis indicators\. public static class ValidationHelper ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 ValidationHelper +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ ValidationHelper ### Remarks This helper class consolidates repetitive validation logic that appears across diff --git a/docs/functions/AcosResult.md b/docs/functions/AcosResult.md index 6873a9ec..154b6c71 100644 --- a/docs/functions/AcosResult.md +++ b/docs/functions/AcosResult.md @@ -9,7 +9,7 @@ Represents the result of an Acos \(Arc Cosine\) calculation\. public record AcosResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AcosResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AcosResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AcosResult](AcosResult.md 'TechnicalAnalysis\.Functions\.AcosResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AdOscResult.md b/docs/functions/AdOscResult.md index 609059ff..06117063 100644 --- a/docs/functions/AdOscResult.md +++ b/docs/functions/AdOscResult.md @@ -9,7 +9,7 @@ Represents the result of the Chaikin Accumulation/Distribution Oscillator \(A/D public record AdOscResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AdOscResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AdOscResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AdOscResult](AdOscResult.md 'TechnicalAnalysis\.Functions\.AdOscResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AdResult.md b/docs/functions/AdResult.md index 7030482e..0c0df84b 100644 --- a/docs/functions/AdResult.md +++ b/docs/functions/AdResult.md @@ -9,7 +9,7 @@ Represents the result of the Accumulation/Distribution Line \(A/D\) indicator ca public record AdResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AdResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AdResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AdResult](AdResult.md 'TechnicalAnalysis\.Functions\.AdResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AddResult.md b/docs/functions/AddResult.md index 3b43239b..4f7f3b69 100644 --- a/docs/functions/AddResult.md +++ b/docs/functions/AddResult.md @@ -9,7 +9,7 @@ Represents the result of the vector addition operation \(ADD function\)\. public record AddResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AddResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AddResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AddResult](AddResult.md 'TechnicalAnalysis\.Functions\.AddResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AdxResult.md b/docs/functions/AdxResult.md index f3e5d9d3..1ced668d 100644 --- a/docs/functions/AdxResult.md +++ b/docs/functions/AdxResult.md @@ -9,7 +9,7 @@ Represents the result of an ADX \(Average Directional Index\) calculation\. public record AdxResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AdxResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AdxResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AdxResult](AdxResult.md 'TechnicalAnalysis\.Functions\.AdxResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AdxrResult.md b/docs/functions/AdxrResult.md index 51be4a15..ef5cc967 100644 --- a/docs/functions/AdxrResult.md +++ b/docs/functions/AdxrResult.md @@ -11,7 +11,7 @@ of trend strength by averaging the current ADX value with a previous ADX value\. public record AdxrResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AdxrResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AdxrResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AdxrResult](AdxrResult.md 'TechnicalAnalysis\.Functions\.AdxrResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/ApoResult.md b/docs/functions/ApoResult.md index 6b7eccb2..d8bc065f 100644 --- a/docs/functions/ApoResult.md +++ b/docs/functions/ApoResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Absolute Price Oscillator \(APO\) indic public record ApoResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 ApoResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ ApoResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[ApoResult](ApoResult.md 'TechnicalAnalysis\.Functions\.ApoResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AroonOscResult.md b/docs/functions/AroonOscResult.md index b4e3c7c8..17226a7b 100644 --- a/docs/functions/AroonOscResult.md +++ b/docs/functions/AroonOscResult.md @@ -9,7 +9,7 @@ Represents the result of the Aroon Oscillator calculation\. public record AroonOscResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AroonOscResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AroonOscResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AroonOscResult](AroonOscResult.md 'TechnicalAnalysis\.Functions\.AroonOscResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AroonResult.md b/docs/functions/AroonResult.md index a528a66f..c05f7365 100644 --- a/docs/functions/AroonResult.md +++ b/docs/functions/AroonResult.md @@ -9,7 +9,7 @@ Represents the result of the Aroon indicator calculation\. public record AroonResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 AroonResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ AroonResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AroonResult](AroonResult.md 'TechnicalAnalysis\.Functions\.AroonResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AsinResult.md b/docs/functions/AsinResult.md index f5598140..e8e0e226 100644 --- a/docs/functions/AsinResult.md +++ b/docs/functions/AsinResult.md @@ -9,7 +9,7 @@ Represents the result of the vector arcsine operation \(ASIN function\)\. public record AsinResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AsinResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AsinResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AsinResult](AsinResult.md 'TechnicalAnalysis\.Functions\.AsinResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AtanResult.md b/docs/functions/AtanResult.md index 42020932..446d4bbd 100644 --- a/docs/functions/AtanResult.md +++ b/docs/functions/AtanResult.md @@ -9,7 +9,7 @@ Represents the result of the vector arctangent operation \(ATAN function\)\. public record AtanResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AtanResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AtanResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AtanResult](AtanResult.md 'TechnicalAnalysis\.Functions\.AtanResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/AtrResult.md b/docs/functions/AtrResult.md index 997f5e15..21b6c66a 100644 --- a/docs/functions/AtrResult.md +++ b/docs/functions/AtrResult.md @@ -10,7 +10,7 @@ This volatility indicator measures the average of true ranges over a specified p public record AtrResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AtrResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AtrResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AtrResult](AtrResult.md 'TechnicalAnalysis\.Functions\.AtrResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/Atypical.TechnicalAnalysis.Functions.md b/docs/functions/Atypical.TechnicalAnalysis.Functions.md index f16de654..816b4b6e 100644 --- a/docs/functions/Atypical.TechnicalAnalysis.Functions.md +++ b/docs/functions/Atypical.TechnicalAnalysis.Functions.md @@ -147,6 +147,11 @@ - **[MinusDIResult\(RetCode, int, int, double\[\]\)](MinusDIResult.MinusDIResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.MinusDIResult\.MinusDIResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [MinusDIResult](MinusDIResult.md 'TechnicalAnalysis\.Functions\.MinusDIResult') class\. - **[MinusDMResult](MinusDMResult.md 'TechnicalAnalysis\.Functions\.MinusDMResult')** `Class` Represents the result of the Minus Directional Movement \(\-DM\) indicator calculation\. \-DM measures downward price movement and is used as a component in calculating the Directional Movement System indicators\. - **[MinusDMResult\(RetCode, int, int, double\[\]\)](MinusDMResult.MinusDMResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.MinusDMResult\.MinusDMResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [MinusDMResult](MinusDMResult.md 'TechnicalAnalysis\.Functions\.MinusDMResult') class\. +- **[MomentumIndicators](MomentumIndicators.md 'TechnicalAnalysis\.Functions\.MomentumIndicators')** `Class` Fluent momentum indicators\. + - **[Adx\(this PriceSeries, int\)](MomentumIndicators.Adx(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Adx\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)')** `Method` Computes the average directional index β€” the strength of a trend, without its direction\. + - **[Macd\(this PriceSeries, int, int, int\)](MomentumIndicators.Macd(thisPriceSeries,int,int,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Macd\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, int\)')** `Method` Computes the moving average convergence divergence of the closing prices\. + - **[Rsi\(this PriceSeries, int\)](MomentumIndicators.Rsi(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Rsi\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)')** `Method` Computes the relative strength index of the closing prices\. + - **[Stoch\(this PriceSeries, int, int, MAType, int, MAType\)](MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Stoch\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, TechnicalAnalysis\.Common\.MAType, int, TechnicalAnalysis\.Common\.MAType\)')** `Method` Computes the slow stochastic oscillator\. - **[MomResult](MomResult.md 'TechnicalAnalysis\.Functions\.MomResult')** `Class` Represents the result of calculating the Momentum \(MOM\) indicator\. - **[MomResult\(RetCode, int, int, double\[\]\)](MomResult.MomResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.MomResult\.MomResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [MomResult](MomResult.md 'TechnicalAnalysis\.Functions\.MomResult') class\. - **[MovingAverageResult](MovingAverageResult.md 'TechnicalAnalysis\.Functions\.MovingAverageResult')** `Class` Represents the result of calculating a Moving Average indicator\. @@ -159,6 +164,10 @@ - **[NatrResult\(RetCode, int, int, double\[\]\)](NatrResult.NatrResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.NatrResult\.NatrResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [NatrResult](NatrResult.md 'TechnicalAnalysis\.Functions\.NatrResult') class\. - **[ObvResult](ObvResult.md 'TechnicalAnalysis\.Functions\.ObvResult')** `Class` Represents the result of the On Balance Volume \(OBV\) indicator calculation\. - **[ObvResult\(RetCode, int, int, double\[\]\)](ObvResult.ObvResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.ObvResult\.ObvResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [ObvResult](ObvResult.md 'TechnicalAnalysis\.Functions\.ObvResult') class\. +- **[OverlapStudyIndicators](OverlapStudyIndicators.md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators')** `Class` Fluent overlap study indicators β€” those plotted on the price scale\. + - **[BollingerBands\(this PriceSeries, int, double, double, MAType\)](OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.BollingerBands\(this TechnicalAnalysis\.Functions\.PriceSeries, int, double, double, TechnicalAnalysis\.Common\.MAType\)')** `Method` Computes Bollinger Bands over the closing prices\. + - **[Ema\(this PriceSeries, int\)](OverlapStudyIndicators.Ema(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Ema\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)')** `Method` Computes the exponential moving average of the closing prices\. + - **[Sma\(this PriceSeries, int\)](OverlapStudyIndicators.Sma(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Sma\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)')** `Method` Computes the simple moving average of the closing prices\. - **[PlusDIResult](PlusDIResult.md 'TechnicalAnalysis\.Functions\.PlusDIResult')** `Class` Represents the result of the Plus Directional Indicator \(\+DI\) calculation\. \+DI is part of the Directional Movement System and measures the strength of upward price movements\. - **[PlusDIResult\(RetCode, int, int, double\[\]\)](PlusDIResult.PlusDIResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.PlusDIResult\.PlusDIResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [PlusDIResult](PlusDIResult.md 'TechnicalAnalysis\.Functions\.PlusDIResult') class\. - **[PlusDMResult](PlusDMResult.md 'TechnicalAnalysis\.Functions\.PlusDMResult')** `Class` Represents the result of the Plus Directional Movement \(\+DM\) indicator calculation\. \+DM measures upward price movement and is used as a component in calculating the Directional Movement System indicators\. @@ -633,6 +642,10 @@ - **[UltOscResult\(RetCode, int, int, double\[\]\)](UltOscResult.UltOscResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.UltOscResult\.UltOscResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [UltOscResult](UltOscResult.md 'TechnicalAnalysis\.Functions\.UltOscResult') class\. - **[VarianceResult](VarianceResult.md 'TechnicalAnalysis\.Functions\.VarianceResult')** `Class` Represents the result of the Variance indicator calculation\. Variance is a statistical measure of volatility that represents the squared deviations from the mean price\. - **[VarianceResult\(RetCode, int, int, double\[\]\)](VarianceResult.VarianceResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.VarianceResult\.VarianceResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [VarianceResult](VarianceResult.md 'TechnicalAnalysis\.Functions\.VarianceResult') class\. +- **[VolatilityIndicators](VolatilityIndicators.md 'TechnicalAnalysis\.Functions\.VolatilityIndicators')** `Class` Fluent volatility indicators\. + - **[Atr\(this PriceSeries, int\)](VolatilityIndicators.Atr(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.VolatilityIndicators\.Atr\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)')** `Method` Computes the average true range\. +- **[VolumeIndicators](VolumeIndicators.md 'TechnicalAnalysis\.Functions\.VolumeIndicators')** `Class` Fluent volume indicators\. + - **[Obv\(this PriceSeries\)](VolumeIndicators.Obv(thisPriceSeries).md 'TechnicalAnalysis\.Functions\.VolumeIndicators\.Obv\(this TechnicalAnalysis\.Functions\.PriceSeries\)')** `Method` Computes on\-balance volume: the running total of volume, signed by the direction of the close\. - **[WclPriceResult](WclPriceResult.md 'TechnicalAnalysis\.Functions\.WclPriceResult')** `Class` Represents the result of the Weighted Close Price calculation\. - **[WclPriceResult\(RetCode, int, int, double\[\]\)](WclPriceResult.WclPriceResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.WclPriceResult\.WclPriceResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [WclPriceResult](WclPriceResult.md 'TechnicalAnalysis\.Functions\.WclPriceResult') class\. - **[WillRResult](WillRResult.md 'TechnicalAnalysis\.Functions\.WillRResult')** `Class` Represents the result of calculating the Williams' %R \(WillR\) indicator\. @@ -640,4 +653,76 @@ - **[WmaResult](WmaResult.md 'TechnicalAnalysis\.Functions\.WmaResult')** `Class` Represents the result of calculating the Weighted Moving Average \(WMA\) indicator\. - **[WmaResult\(RetCode, int, int, double\[\]\)](WmaResult.WmaResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.WmaResult\.WmaResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [WmaResult](WmaResult.md 'TechnicalAnalysis\.Functions\.WmaResult') class\. - **[ZigZagResult](ZigZagResult.md 'TechnicalAnalysis\.Functions\.ZigZagResult')** `Class` Represents the result of calculating the Zig Zag indicator\. - - **[ZigZagResult\(RetCode, int, int, double\[\]\)](ZigZagResult.ZigZagResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.ZigZagResult\.ZigZagResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [ZigZagResult](ZigZagResult.md 'TechnicalAnalysis\.Functions\.ZigZagResult') class\. \ No newline at end of file + - **[ZigZagResult\(RetCode, int, int, double\[\]\)](ZigZagResult.ZigZagResult(RetCode,int,int,double[]).md 'TechnicalAnalysis\.Functions\.ZigZagResult\.ZigZagResult\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\]\)')** `Constructor` Initializes a new instance of the [ZigZagResult](ZigZagResult.md 'TechnicalAnalysis\.Functions\.ZigZagResult') class\. +- **[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries')** `Struct` The three bar\-aligned bands of a Bollinger Bands calculation\. + - **[BollingerBandsSeries\(IndicatorSeries, IndicatorSeries, IndicatorSeries\)](BollingerBandsSeries.BollingerBandsSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.BollingerBandsSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)')** `Constructor` The three bar\-aligned bands of a Bollinger Bands calculation\. + - **[Lower](BollingerBandsSeries.Lower.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.Lower')** `Property` The lower band: the middle band minus the requested number of standard deviations\. Bar\-aligned\. + - **[Middle](BollingerBandsSeries.Middle.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.Middle')** `Property` The middle band: the moving average of the closing prices\. Bar\-aligned\. + - **[Upper](BollingerBandsSeries.Upper.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.Upper')** `Property` The upper band: the middle band plus the requested number of standard deviations\. Bar\-aligned\. + - **[AsOf\(int\)](BollingerBandsSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.AsOf\(int\)')** `Method` Narrows every band so that it ends at the given BAR index\. +- **[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries')** `Struct` A bar\-aligned view over a single output series produced by a TA\-Lib indicator\. This is the one place in the library where TA\-Lib's raw alignment metadata is interpreted\. + - **[BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')** `Property` Gets the number of bars in the source price series\. + - **[FirstBar](IndicatorSeries.FirstBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.FirstBar')** `Property` Gets the BAR index of the first bar that has a value, or `null` when no bar has one\. + - **[HasValues](IndicatorSeries.HasValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.HasValues')** `Property` Gets a value indicating whether any bar of this series carries a value\. + - **[LastBar](IndicatorSeries.LastBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.LastBar')** `Property` Gets the BAR index of the last bar that has a value, or `null` when no bar has one\. + - **[Latest](IndicatorSeries.Latest.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Latest')** `Property` Gets the most recent value in the series, or `null` when no bar has a value\. + - **[RetCode](IndicatorSeries.RetCode.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.RetCode')** `Property` Gets the return code reported by the underlying TA\-Lib call\. + - **[this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]')** `Property` Gets the value at the given BAR index, or `null` when that bar has no value\. + - **[WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount')** `Property` Gets the number of bars that carry a value β€” TA\-Lib's `NBElement`\. + - **[WarmValues](IndicatorSeries.WarmValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValues')** `Property` Gets the values as a span whose element `k` describes bar `FirstBar + k`\. + - **[AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)')** `Method` Returns the same series truncated so that it ends at the given BAR index, making look\-ahead unrepresentable rather than merely detectable\. + - **[CopyBarAligned\(Span<double>\)](IndicatorSeries.CopyBarAligned(Span_double_).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CopyBarAligned\(System\.Span\\)')** `Method` Writes the series into the given span in which the index is the BAR index\. + - **[Create\(RetCode, int, int, double\[\], int\)](IndicatorSeries.Create(RetCode,int,int,double[],int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)')** `Method` Creates a bar\-aligned series from the raw metadata of a TA\-Lib call\. This is the only point in the library at which raw TA\-Lib alignment metadata enters the type system\. + - **[CrossedAbove\(double, int\)](IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)')** `Method` Determines whether this series crossed above a fixed level at the given BAR index\. + - **[CrossedAbove\(IndicatorSeries, int\)](IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)')** `Method` Determines whether this series crossed above another series at the given BAR index\. + - **[CrossedBelow\(double, int\)](IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)')** `Method` Determines whether this series crossed below a fixed level at the given BAR index\. + - **[CrossedBelow\(IndicatorSeries, int\)](IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)')** `Method` Determines whether this series crossed below another series at the given BAR index\. + - **[Empty\(int\)](IndicatorSeries.Empty(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Empty\(int\)')** `Method` Creates a series covering the given number of bars in which no bar has a value\. + - **[Equals\(object\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(object) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(object\)')** `Method` Determines whether this series equals the given object\. + - **[Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)')** `Method` Determines whether this series equals another\. + - **[GetEnumerator\(\)](IndicatorSeries.GetEnumerator().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.GetEnumerator\(\)')** `Method` Returns an allocation\-free enumerator over the bars of this series that carry a value\. + - **[GetHashCode\(\)](IndicatorSeries.GetHashCode().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.GetHashCode\(\)')** `Method` Returns a hash code consistent with [Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)')\. + - **[IsWarmAt\(int\)](IndicatorSeries.IsWarmAt(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.IsWarmAt\(int\)')** `Method` Determines whether the given BAR index has a value\. + - **[ToBarAlignedArray\(\)](IndicatorSeries.ToBarAlignedArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedArray\(\)')** `Method` Projects the series onto a new array indexed by BAR index, padding bars that have no value with [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN')\. + - **[ToBarAlignedNullableArray\(\)](IndicatorSeries.ToBarAlignedNullableArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedNullableArray\(\)')** `Method` Projects the series onto a new array indexed by BAR index, with `null` for bars that have no value\. + - **[WarmValuesToArray\(\)](IndicatorSeries.WarmValuesToArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValuesToArray\(\)')** `Method` Copies the values into a new array whose element `k` describes bar `FirstBar + k`\. + - **[operator ==\(IndicatorSeries, IndicatorSeries\)](IndicatorSeries.operator(IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.op\_Equality\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)')** `Operator` Determines whether two series are equal\. + - **[operator \!=\(IndicatorSeries, IndicatorSeries\)](IndicatorSeries.operator!(IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.op\_Inequality\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)')** `Operator` Determines whether two series are not equal\. +- **[IndicatorSeries\.Enumerator](IndicatorSeries.Enumerator.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator')** `Struct` Enumerates the warm bars of an [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') in ascending BAR order\. + - **[Current](IndicatorSeries.Enumerator.Current.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.Current')** `Property` Gets the current bar and its value\. + - **[MoveNext\(\)](IndicatorSeries.Enumerator.MoveNext().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.MoveNext\(\)')** `Method` Advances to the next warm bar\. +- **[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries')** `Struct` The three bar\-aligned outputs of a MACD calculation\. + - **[MacdSeries\(IndicatorSeries, IndicatorSeries, IndicatorSeries\)](MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.MacdSeries\.MacdSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)')** `Constructor` The three bar\-aligned outputs of a MACD calculation\. + - **[Histogram](MacdSeries.Histogram.md 'TechnicalAnalysis\.Functions\.MacdSeries\.Histogram')** `Property` The histogram: Line minus Signal\. Bar\-aligned\. + - **[Line](MacdSeries.Line.md 'TechnicalAnalysis\.Functions\.MacdSeries\.Line')** `Property` The MACD line: the fast exponential moving average minus the slow one\. Bar\-aligned\. + - **[Signal](MacdSeries.Signal.md 'TechnicalAnalysis\.Functions\.MacdSeries\.Signal')** `Property` The signal line: an exponential moving average of Line\. Bar\-aligned\. + - **[AsOf\(int\)](MacdSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.MacdSeries\.AsOf\(int\)')** `Method` Narrows every component so that it ends at the given BAR index\. +- **[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries')** `Struct` An immutable price series β€” the entry point to the fluent indicator API\. + - **[BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount')** `Property` Gets the number of bars in the series\. + - **[Close](PriceSeries.Close.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Close')** `Property` Gets the closing prices\. + - **[Empty](PriceSeries.Empty.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Empty')** `Property` Gets the empty price series\. + - **[HasHighLow](PriceSeries.HasHighLow.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasHighLow')** `Property` Gets a value indicating whether the series carries high and low prices\. + - **[HasOpen](PriceSeries.HasOpen.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasOpen')** `Property` Gets a value indicating whether the series carries opening prices\. + - **[HasVolume](PriceSeries.HasVolume.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasVolume')** `Property` Gets a value indicating whether the series carries volumes\. + - **[High](PriceSeries.High.md 'TechnicalAnalysis\.Functions\.PriceSeries\.High')** `Property` Gets the high prices\. + - **[IsEmpty](PriceSeries.IsEmpty.md 'TechnicalAnalysis\.Functions\.PriceSeries\.IsEmpty')** `Property` Gets a value indicating whether the series holds no bars\. + - **[Low](PriceSeries.Low.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Low')** `Property` Gets the low prices\. + - **[Open](PriceSeries.Open.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Open')** `Property` Gets the opening prices\. + - **[Volume](PriceSeries.Volume.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Volume')** `Property` Gets the volumes\. + - **[Align\(SingleOutputResult\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)')** `Method` Bar\-aligns the result of any single\-output `TAMath` call made over this price series\. + - **[Align<TResult>\(TResult, Func<TResult,double\[\]>\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)')** `Method` Bar\-aligns one output of any multi\-output `TAMath` call made over this price series\. + - **[AsOf\(int\)](PriceSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.PriceSeries\.AsOf\(int\)')** `Method` Returns the same price series truncated so that it ends at the given BAR index\. + - **[Equals\(object\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(object) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(object\)')** `Method` Determines whether this price series equals the given object\. + - **[Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)')** `Method` Determines whether this price series equals another\. + - **[FromClose\(ReadOnlySpan<double>\)](PriceSeries.FromClose(ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromClose\(System\.ReadOnlySpan\\)')** `Method` Creates a price series from closing prices alone\. + - **[FromHlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)')** `Method` Creates a price series from high, low and closing prices\. + - **[FromOhlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)')** `Method` Creates a price series from open, high, low and closing prices\. + - **[FromOhlcv\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)')** `Method` Creates a price series from open, high, low and closing prices together with volumes\. + - **[GetHashCode\(\)](PriceSeries.GetHashCode().md 'TechnicalAnalysis\.Functions\.PriceSeries\.GetHashCode\(\)')** `Method` Returns a hash code consistent with [Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)')\. + - **[operator ==\(PriceSeries, PriceSeries\)](PriceSeries.operator(PriceSeries,PriceSeries).md 'TechnicalAnalysis\.Functions\.PriceSeries\.op\_Equality\(TechnicalAnalysis\.Functions\.PriceSeries, TechnicalAnalysis\.Functions\.PriceSeries\)')** `Operator` Determines whether two price series are equal\. + - **[operator \!=\(PriceSeries, PriceSeries\)](PriceSeries.operator!(PriceSeries,PriceSeries).md 'TechnicalAnalysis\.Functions\.PriceSeries\.op\_Inequality\(TechnicalAnalysis\.Functions\.PriceSeries, TechnicalAnalysis\.Functions\.PriceSeries\)')** `Operator` Determines whether two price series are not equal\. +- **[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries')** `Struct` The two bar\-aligned outputs of a stochastic oscillator calculation\. + - **[StochSeries\(IndicatorSeries, IndicatorSeries\)](StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.StochSeries\.StochSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)')** `Constructor` The two bar\-aligned outputs of a stochastic oscillator calculation\. + - **[SlowD](StochSeries.SlowD.md 'TechnicalAnalysis\.Functions\.StochSeries\.SlowD')** `Property` The slow %D line: a moving average of SlowK, acting as its signal line\. Bar\-aligned\. + - **[SlowK](StochSeries.SlowK.md 'TechnicalAnalysis\.Functions\.StochSeries\.SlowK')** `Property` The slow %K line: the smoothed position of the close within the recent high\-low range, expressed from 0 to 100\. Bar\-aligned\. + - **[AsOf\(int\)](StochSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.StochSeries\.AsOf\(int\)')** `Method` Narrows both lines so that they end at the given BAR index\. \ No newline at end of file diff --git a/docs/functions/AvgPriceResult.md b/docs/functions/AvgPriceResult.md index 196d23b3..ba99ac79 100644 --- a/docs/functions/AvgPriceResult.md +++ b/docs/functions/AvgPriceResult.md @@ -9,7 +9,7 @@ Represents the result of the Average Price calculation\. public record AvgPriceResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 AvgPriceResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ AvgPriceResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[AvgPriceResult](AvgPriceResult.md 'TechnicalAnalysis\.Functions\.AvgPriceResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/BetaResult.md b/docs/functions/BetaResult.md index 65d65db7..bdf4b83a 100644 --- a/docs/functions/BetaResult.md +++ b/docs/functions/BetaResult.md @@ -9,7 +9,7 @@ Represents the result of the Beta coefficient calculation\. public record BetaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 BetaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ BetaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[BetaResult](BetaResult.md 'TechnicalAnalysis\.Functions\.BetaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/BollingerBandsResult.md b/docs/functions/BollingerBandsResult.md index 3e434226..9f381d89 100644 --- a/docs/functions/BollingerBandsResult.md +++ b/docs/functions/BollingerBandsResult.md @@ -10,7 +10,7 @@ Bollinger Bands consist of a middle band \(SMA\) and two outer bands that repres public record BollingerBandsResult : TechnicalAnalysis.Common.TripleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') 🡒 BollingerBandsResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') β†’ BollingerBandsResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[BollingerBandsResult](BollingerBandsResult.md 'TechnicalAnalysis\.Functions\.BollingerBandsResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/BollingerBandsSeries.AsOf(int).md b/docs/functions/BollingerBandsSeries.AsOf(int).md new file mode 100644 index 00000000..04e53464 --- /dev/null +++ b/docs/functions/BollingerBandsSeries.AsOf(int).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') + +## BollingerBandsSeries\.AsOf\(int\) Method + +Narrows every band so that it ends at the given BAR index\. + +```csharp +public TechnicalAnalysis.Functions.BollingerBandsSeries AsOf(int bar); +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The last BAR index the narrowed bands are allowed to know about, with domain +`[0, BarCount)` of the bands\. + +#### Returns +[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') +A result whose three bands have each been narrowed by [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)')\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](BollingerBandsSeries.AsOf(int).md#TechnicalAnalysis.Functions.BollingerBandsSeries.AsOf(int).bar 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.AsOf\(int\)\.bar') is outside the bars the bands cover\. \ No newline at end of file diff --git a/docs/functions/BollingerBandsSeries.BollingerBandsSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md b/docs/functions/BollingerBandsSeries.BollingerBandsSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md new file mode 100644 index 00000000..c5e478f8 --- /dev/null +++ b/docs/functions/BollingerBandsSeries.BollingerBandsSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md @@ -0,0 +1,34 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') + +## BollingerBandsSeries\(IndicatorSeries, IndicatorSeries, IndicatorSeries\) Constructor + +The three bar\-aligned bands of a Bollinger Bands calculation\. + +```csharp +public BollingerBandsSeries(TechnicalAnalysis.Functions.IndicatorSeries Upper, TechnicalAnalysis.Functions.IndicatorSeries Middle, TechnicalAnalysis.Functions.IndicatorSeries Lower); +``` +#### Parameters + + + +`Upper` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The upper band: the middle band plus the requested number of standard deviations\. Bar\-aligned\. + + + +`Middle` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The middle band: the moving average of the closing prices\. Bar\-aligned\. + + + +`Lower` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The lower band: the middle band minus the requested number of standard deviations\. Bar\-aligned\. + +### Remarks +Each band is an independently addressable [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries')\. Derived measures such +as %B or bandwidth are deliberately not provided: they are arithmetic on three numbers the +caller already has, and every convention for them is an opinion\. \ No newline at end of file diff --git a/docs/functions/BollingerBandsSeries.Lower.md b/docs/functions/BollingerBandsSeries.Lower.md new file mode 100644 index 00000000..aa6e712f --- /dev/null +++ b/docs/functions/BollingerBandsSeries.Lower.md @@ -0,0 +1,13 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') + +## BollingerBandsSeries\.Lower Property + +The lower band: the middle band minus the requested number of standard deviations\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Lower { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/BollingerBandsSeries.Middle.md b/docs/functions/BollingerBandsSeries.Middle.md new file mode 100644 index 00000000..48d0bd62 --- /dev/null +++ b/docs/functions/BollingerBandsSeries.Middle.md @@ -0,0 +1,13 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') + +## BollingerBandsSeries\.Middle Property + +The middle band: the moving average of the closing prices\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Middle { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/BollingerBandsSeries.Upper.md b/docs/functions/BollingerBandsSeries.Upper.md new file mode 100644 index 00000000..41e5c15e --- /dev/null +++ b/docs/functions/BollingerBandsSeries.Upper.md @@ -0,0 +1,13 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') + +## BollingerBandsSeries\.Upper Property + +The upper band: the middle band plus the requested number of standard deviations\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Upper { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/BollingerBandsSeries.md b/docs/functions/BollingerBandsSeries.md new file mode 100644 index 00000000..20b15717 --- /dev/null +++ b/docs/functions/BollingerBandsSeries.md @@ -0,0 +1,31 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## BollingerBandsSeries Struct + +The three bar\-aligned bands of a Bollinger Bands calculation\. + +```csharp +public readonly record struct BollingerBandsSeries : System.IEquatable +``` + +Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') + +### Remarks +Each band is an independently addressable [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries')\. Derived measures such +as %B or bandwidth are deliberately not provided: they are arithmetic on three numbers the +caller already has, and every convention for them is an opinion\. + +| Constructors | | +| :--- | :--- | +| [BollingerBandsSeries\(IndicatorSeries, IndicatorSeries, IndicatorSeries\)](BollingerBandsSeries.BollingerBandsSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.BollingerBandsSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)') | The three bar\-aligned bands of a Bollinger Bands calculation\. | + +| Properties | | +| :--- | :--- | +| [Lower](BollingerBandsSeries.Lower.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.Lower') | The lower band: the middle band minus the requested number of standard deviations\. Bar\-aligned\. | +| [Middle](BollingerBandsSeries.Middle.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.Middle') | The middle band: the moving average of the closing prices\. Bar\-aligned\. | +| [Upper](BollingerBandsSeries.Upper.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.Upper') | The upper band: the middle band plus the requested number of standard deviations\. Bar\-aligned\. | + +| Methods | | +| :--- | :--- | +| [AsOf\(int\)](BollingerBandsSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries\.AsOf\(int\)') | Narrows every band so that it ends at the given BAR index\. | diff --git a/docs/functions/BopResult.md b/docs/functions/BopResult.md index 0b93f07f..60edeac2 100644 --- a/docs/functions/BopResult.md +++ b/docs/functions/BopResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Balance Of Power \(BOP\) indicator\. public record BopResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 BopResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ BopResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[BopResult](BopResult.md 'TechnicalAnalysis\.Functions\.BopResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/CciResult.md b/docs/functions/CciResult.md index bd0e04a6..bd097308 100644 --- a/docs/functions/CciResult.md +++ b/docs/functions/CciResult.md @@ -10,7 +10,7 @@ CCI is a momentum oscillator that measures the difference between a security's p public record CciResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 CciResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ CciResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CciResult](CciResult.md 'TechnicalAnalysis\.Functions\.CciResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/CeilResult.md b/docs/functions/CeilResult.md index 229f69a6..005bea7d 100644 --- a/docs/functions/CeilResult.md +++ b/docs/functions/CeilResult.md @@ -9,7 +9,7 @@ Represents the result of the vector ceiling operation \(CEIL function\)\. public record CeilResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 CeilResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ CeilResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CeilResult](CeilResult.md 'TechnicalAnalysis\.Functions\.CeilResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/CmoResult.md b/docs/functions/CmoResult.md index 9eb51ec9..e3ad0be9 100644 --- a/docs/functions/CmoResult.md +++ b/docs/functions/CmoResult.md @@ -11,7 +11,7 @@ over a specified period, normalized to oscillate between \-100 and \+100\. public record CmoResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 CmoResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ CmoResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CmoResult](CmoResult.md 'TechnicalAnalysis\.Functions\.CmoResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/CorrelResult.md b/docs/functions/CorrelResult.md index 7d7270db..8145f7e6 100644 --- a/docs/functions/CorrelResult.md +++ b/docs/functions/CorrelResult.md @@ -9,7 +9,7 @@ Represents the result of the Pearson Correlation Coefficient calculation\. public record CorrelResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 CorrelResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ CorrelResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CorrelResult](CorrelResult.md 'TechnicalAnalysis\.Functions\.CorrelResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/CosResult.md b/docs/functions/CosResult.md index 5692142e..4ed833e8 100644 --- a/docs/functions/CosResult.md +++ b/docs/functions/CosResult.md @@ -9,7 +9,7 @@ Represents the result of the vector cosine operation \(COS function\)\. public record CosResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 CosResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ CosResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CosResult](CosResult.md 'TechnicalAnalysis\.Functions\.CosResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/CoshResult.md b/docs/functions/CoshResult.md index 713af2b9..26e4d16c 100644 --- a/docs/functions/CoshResult.md +++ b/docs/functions/CoshResult.md @@ -9,7 +9,7 @@ Represents the result of the vector hyperbolic cosine operation \(COSH function\ public record CoshResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 CoshResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ CoshResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[CoshResult](CoshResult.md 'TechnicalAnalysis\.Functions\.CoshResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/DemaResult.md b/docs/functions/DemaResult.md index 8f13c4d0..830e1094 100644 --- a/docs/functions/DemaResult.md +++ b/docs/functions/DemaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Double Exponential Moving Average \(DEM public record DemaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 DemaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ DemaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[DemaResult](DemaResult.md 'TechnicalAnalysis\.Functions\.DemaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/DivResult.md b/docs/functions/DivResult.md index cdb52868..854b77c2 100644 --- a/docs/functions/DivResult.md +++ b/docs/functions/DivResult.md @@ -9,7 +9,7 @@ Represents the result of the vector division operation \(DIV function\)\. public record DivResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 DivResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ DivResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[DivResult](DivResult.md 'TechnicalAnalysis\.Functions\.DivResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/DxResult.md b/docs/functions/DxResult.md index 0a17fea4..f9fcf8c5 100644 --- a/docs/functions/DxResult.md +++ b/docs/functions/DxResult.md @@ -10,7 +10,7 @@ DX measures the strength of a trend regardless of its direction, derived from co public record DxResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 DxResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ DxResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[DxResult](DxResult.md 'TechnicalAnalysis\.Functions\.DxResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/EmaResult.md b/docs/functions/EmaResult.md index 8e945fa9..4a2f44d0 100644 --- a/docs/functions/EmaResult.md +++ b/docs/functions/EmaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Exponential Moving Average \(EMA\) indi public record EmaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 EmaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ EmaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[EmaResult](EmaResult.md 'TechnicalAnalysis\.Functions\.EmaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/ExpResult.md b/docs/functions/ExpResult.md index 7261b4a2..dc1f56d3 100644 --- a/docs/functions/ExpResult.md +++ b/docs/functions/ExpResult.md @@ -9,7 +9,7 @@ Represents the result of the vector exponential operation \(EXP function\)\. public record ExpResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 ExpResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ ExpResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[ExpResult](ExpResult.md 'TechnicalAnalysis\.Functions\.ExpResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/FloorResult.md b/docs/functions/FloorResult.md index 5546a89f..b01be81b 100644 --- a/docs/functions/FloorResult.md +++ b/docs/functions/FloorResult.md @@ -9,7 +9,7 @@ Represents the result of the vector floor operation \(FLOOR function\)\. public record FloorResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 FloorResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ FloorResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[FloorResult](FloorResult.md 'TechnicalAnalysis\.Functions\.FloorResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/HtDcPeriodResult.md b/docs/functions/HtDcPeriodResult.md index c919f290..77f1ea5a 100644 --- a/docs/functions/HtDcPeriodResult.md +++ b/docs/functions/HtDcPeriodResult.md @@ -11,7 +11,7 @@ providing insight into the cyclical nature of price movements\. public record HtDcPeriodResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 HtDcPeriodResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ HtDcPeriodResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[HtDcPeriodResult](HtDcPeriodResult.md 'TechnicalAnalysis\.Functions\.HtDcPeriodResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/HtDcPhaseResult.md b/docs/functions/HtDcPhaseResult.md index 94c43c1b..4f8021d8 100644 --- a/docs/functions/HtDcPhaseResult.md +++ b/docs/functions/HtDcPhaseResult.md @@ -11,7 +11,7 @@ helping to identify the current position within a price cycle\. public record HtDcPhaseResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 HtDcPhaseResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ HtDcPhaseResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[HtDcPhaseResult](HtDcPhaseResult.md 'TechnicalAnalysis\.Functions\.HtDcPhaseResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/HtPhasorResult.md b/docs/functions/HtPhasorResult.md index 544eb3cd..56d0069f 100644 --- a/docs/functions/HtPhasorResult.md +++ b/docs/functions/HtPhasorResult.md @@ -11,7 +11,7 @@ providing a complex representation of market cycles for advanced signal processi public record HtPhasorResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 HtPhasorResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ HtPhasorResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[HtPhasorResult](HtPhasorResult.md 'TechnicalAnalysis\.Functions\.HtPhasorResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/HtSineResult.md b/docs/functions/HtSineResult.md index e4a21dac..dd475021 100644 --- a/docs/functions/HtSineResult.md +++ b/docs/functions/HtSineResult.md @@ -11,7 +11,7 @@ useful for identifying cycle turns and generating trading signals in trending ma public record HtSineResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 HtSineResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ HtSineResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[HtSineResult](HtSineResult.md 'TechnicalAnalysis\.Functions\.HtSineResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/HtTrendModeResult.md b/docs/functions/HtTrendModeResult.md index ea139b58..a8bdc51a 100644 --- a/docs/functions/HtTrendModeResult.md +++ b/docs/functions/HtTrendModeResult.md @@ -11,7 +11,7 @@ helping traders choose appropriate strategies for different market conditions\. public record HtTrendModeResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 HtTrendModeResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ HtTrendModeResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[HtTrendModeResult](HtTrendModeResult.md 'TechnicalAnalysis\.Functions\.HtTrendModeResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/HtTrendlineResult.md b/docs/functions/HtTrendlineResult.md index 0a09930b..150820ac 100644 --- a/docs/functions/HtTrendlineResult.md +++ b/docs/functions/HtTrendlineResult.md @@ -11,7 +11,7 @@ effectively filtering out short\-term fluctuations to reveal the underlying tren public record HtTrendlineResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 HtTrendlineResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ HtTrendlineResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[HtTrendlineResult](HtTrendlineResult.md 'TechnicalAnalysis\.Functions\.HtTrendlineResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/IndicatorSeries.AsOf(int).md b/docs/functions/IndicatorSeries.AsOf(int).md new file mode 100644 index 00000000..a720a000 --- /dev/null +++ b/docs/functions/IndicatorSeries.AsOf(int).md @@ -0,0 +1,43 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.AsOf\(int\) Method + +Returns the same series truncated so that it ends at the given BAR index, making look\-ahead +unrepresentable rather than merely detectable\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries AsOf(int bar); +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The last BAR index the narrowed series is allowed to know about, with domain +`[0, BarCount)`\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A series with [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') equal to `bar + 1` and the same +[FirstBar](IndicatorSeries.FirstBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.FirstBar'), holding only the values at bars up to and including +[bar](IndicatorSeries.AsOf(int).md#TechnicalAnalysis.Functions.IndicatorSeries.AsOf(int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)\.bar')\. If no value survives, the result carries none at all\. Bar indices +are \not\ rebased: they remain absolute positions in the original price series\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.AsOf(int).md#TechnicalAnalysis.Functions.IndicatorSeries.AsOf(int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. + +### Remarks + +Asking the narrowed series about a later bar throws, because that bar is outside its domain: +`series.AsOf(50)[51]` is an [System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException'), not a value and +not `null`. The future is simply not part of the value handed over. + +All shipped indicators are causal, so narrowing the end never changes an earlier value: +`prices.AsOf(bar).Sma(30).Latest` equals `prices.Sma(30).AsOf(bar).Latest` +exactly. This is the reason to prefer computing once and narrowing per bar β€” which is +allocation-free and O(1) β€” over recomputing the indicator inside a per-bar loop, which is +O(nΒ²). \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.BarCount.md b/docs/functions/IndicatorSeries.BarCount.md new file mode 100644 index 00000000..cda579ec --- /dev/null +++ b/docs/functions/IndicatorSeries.BarCount.md @@ -0,0 +1,16 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.BarCount Property + +Gets the number of bars in the source price series\. + +```csharp +public int BarCount { get; } +``` + +#### Property Value +[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') +A count, not an index\. The domain of every bar index on this type is +`[0, BarCount)`, so this β€” not [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') β€” is the bound of a loop over +[this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]')\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.CopyBarAligned(Span_double_).md b/docs/functions/IndicatorSeries.CopyBarAligned(Span_double_).md new file mode 100644 index 00000000..ab1c4c20 --- /dev/null +++ b/docs/functions/IndicatorSeries.CopyBarAligned(Span_double_).md @@ -0,0 +1,29 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.CopyBarAligned\(Span\\) Method + +Writes the series into the given span in which the index is the BAR index\. + +```csharp +public void CopyBarAligned(System.Span destination); +``` +#### Parameters + + + +`destination` [System\.Span<](https://learn.microsoft.com/en-us/dotnet/api/system.span-1 'System\.Span\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.span-1 'System\.Span\`1') + +The span to write into\. Its first [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') elements are overwritten, so that +element `i` describes bar `i`; bars that have no value are written as +[System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN')\. Any elements beyond [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') are left untouched\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[destination](IndicatorSeries.CopyBarAligned(Span_double_).md#TechnicalAnalysis.Functions.IndicatorSeries.CopyBarAligned(System.Span_double_).destination 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CopyBarAligned\(System\.Span\\)\.destination') is shorter than [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. + +### Remarks +The [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') padding carries the same caveat as +[ToBarAlignedArray\(\)](IndicatorSeries.ToBarAlignedArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedArray\(\)'): it is a sentinel, and it cannot be told apart from a +computed non\-finite value by inspecting the destination alone\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.Create(RetCode,int,int,double[],int).md b/docs/functions/IndicatorSeries.Create(RetCode,int,int,double[],int).md new file mode 100644 index 00000000..c64c201e --- /dev/null +++ b/docs/functions/IndicatorSeries.Create(RetCode,int,int,double[],int).md @@ -0,0 +1,68 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.Create\(RetCode, int, int, double\[\], int\) Method + +Creates a bar\-aligned series from the raw metadata of a TA\-Lib call\. This is the only point +in the library at which raw TA\-Lib alignment metadata enters the type system\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Create(TechnicalAnalysis.Common.RetCode retCode, int begIdx, int nbElement, double[] values, int barCount); +``` +#### Parameters + + + +`retCode` [TechnicalAnalysis\.Common\.RetCode](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode 'TechnicalAnalysis\.Common\.RetCode') + +The return code reported by the TA\-Lib call\. + + + +`begIdx` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +TA\-Lib's `BegIdx`: the BAR index described by output array element `0`\. It is not +examined at all when [nbElement](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).nbElement 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.nbElement') is zero, because TA\-Lib reports +`BegIdx == 0` in that state and the value is meaningless\. + + + +`nbElement` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +TA\-Lib's `NBElement`: the number of elements it actually wrote, starting at ARRAY index +`0`\. This is a count, not an index\. + + + +`values` [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array') + +The raw output array\. Its first [nbElement](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).nbElement 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.nbElement') elements are \copied\, so +the caller keeps sole ownership of the array it passed and may mutate it afterwards without +affecting the series that was handed back\. That is what makes the immutability of this type +unconditional rather than a convention the caller has to honour\. + + + +`barCount` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The number of bars in the source price series\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A series in which every position is addressed by BAR index\. + +#### Exceptions + +[System\.ArgumentNullException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception 'System\.ArgumentNullException') +[values](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).values 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.values') is `null`\. + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[begIdx](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).begIdx 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.begIdx'), [nbElement](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).nbElement 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.nbElement') or [barCount](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).barCount 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.barCount') is negative\. + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[nbElement](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).nbElement 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.nbElement') exceeds the length of [values](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).values 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.values'), or + `begIdx + nbElement` exceeds [barCount](IndicatorSeries.Create(RetCode,int,int,double[],int).md#TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,int,int,double[],int).barCount 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)\.barCount')\. The second check enforces + TA\-Lib's own invariant: the last described bar is `begIdx + nbElement - 1`, which must + fall inside the source series\. A result that violates it is misaligned at its source, and + clamping it here would produce a silently shifted series β€” precisely the failure this type + exists to prevent β€” so it is surfaced loudly instead\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.CrossedAbove.md b/docs/functions/IndicatorSeries.CrossedAbove.md new file mode 100644 index 00000000..ef1889cf --- /dev/null +++ b/docs/functions/IndicatorSeries.CrossedAbove.md @@ -0,0 +1,86 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.CrossedAbove Method + +| Overloads | | +| :--- | :--- | +| [CrossedAbove\(double, int\)](IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)') | Determines whether this series crossed above a fixed level at the given BAR index\. | +| [CrossedAbove\(IndicatorSeries, int\)](IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)') | Determines whether this series crossed above another series at the given BAR index\. | + + + +## IndicatorSeries\.CrossedAbove\(double, int\) Method + +Determines whether this series crossed above a fixed level at the given BAR index\. + +```csharp +public bool CrossedAbove(double level, int bar); +``` +#### Parameters + + + +`level` [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double') + +The level to test against\. + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The BAR index at which the crossing is tested, with domain `[0, BarCount)`\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the value at [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)\.bar') is strictly above + [level](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int).level 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)\.level') and the value at `bar - 1` was at or below it\. A crossing is a + transition between two bars, not a state: a series that is already above the level does not + keep reporting a crossing\. Returns `false` when [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)\.bar') is `0`, or + when either bar has no value\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. + + + +## IndicatorSeries\.CrossedAbove\(IndicatorSeries, int\) Method + +Determines whether this series crossed above another series at the given BAR index\. + +```csharp +public bool CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries other, int bar); +``` +#### Parameters + + + +`other` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The series to cross against\. It must cover the same number of bars as this one, so that a +bar index means the same thing in both\. + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The BAR index at which the crossing is tested, with domain `[0, BarCount)`\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when this series is strictly above [other](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int).other 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.other') at + [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.bar') and was at or below it at `bar - 1`\. Returns `false` when + [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.bar') is `0`, or when any of the four values involved is missing β€” + which is what makes a fast/slow crossing with different warm\-ups work without any reasoning + at the call site\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[other](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int).other 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.other') covers a different number of bars than this series\. Crossing two + series computed over different price series is a caller bug\. + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.CrossedBelow.md b/docs/functions/IndicatorSeries.CrossedBelow.md new file mode 100644 index 00000000..9d302dc9 --- /dev/null +++ b/docs/functions/IndicatorSeries.CrossedBelow.md @@ -0,0 +1,80 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.CrossedBelow Method + +| Overloads | | +| :--- | :--- | +| [CrossedBelow\(double, int\)](IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)') | Determines whether this series crossed below a fixed level at the given BAR index\. | +| [CrossedBelow\(IndicatorSeries, int\)](IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)') | Determines whether this series crossed below another series at the given BAR index\. | + + + +## IndicatorSeries\.CrossedBelow\(double, int\) Method + +Determines whether this series crossed below a fixed level at the given BAR index\. + +```csharp +public bool CrossedBelow(double level, int bar); +``` +#### Parameters + + + +`level` [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double') + +The level to test against\. + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The BAR index at which the crossing is tested, with domain `[0, BarCount)`\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the value at [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)\.bar') is strictly below + [level](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int).level 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)\.level') and the value at `bar - 1` was at or above it\. Returns + `false` when [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)\.bar') is `0`, or when either bar has no value\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. + + + +## IndicatorSeries\.CrossedBelow\(IndicatorSeries, int\) Method + +Determines whether this series crossed below another series at the given BAR index\. + +```csharp +public bool CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries other, int bar); +``` +#### Parameters + + + +`other` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The series to cross against\. It must cover the same number of bars as this one\. + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The BAR index at which the crossing is tested, with domain `[0, BarCount)`\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when this series is strictly below [other](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int).other 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.other') at + [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.bar') and was at or above it at `bar - 1`\. Returns `false` when + [bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.bar') is `0`, or when any of the four values involved is missing\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[other](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int).other 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.other') covers a different number of bars than this series\. + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.Empty(int).md b/docs/functions/IndicatorSeries.Empty(int).md new file mode 100644 index 00000000..d4a5d56e --- /dev/null +++ b/docs/functions/IndicatorSeries.Empty(int).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.Empty\(int\) Method + +Creates a series covering the given number of bars in which no bar has a value\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Empty(int barCount); +``` +#### Parameters + + + +`barCount` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The number of bars in the source price series\. Must not be negative\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A series reporting [TechnicalAnalysis\.Common\.RetCode\.Success](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode.success 'TechnicalAnalysis\.Common\.RetCode\.Success') with [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') zero, +[BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') equal to [barCount](IndicatorSeries.Empty(int).md#TechnicalAnalysis.Functions.IndicatorSeries.Empty(int).barCount 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Empty\(int\)\.barCount'), and [FirstBar](IndicatorSeries.FirstBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.FirstBar')`null`\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[barCount](IndicatorSeries.Empty(int).md#TechnicalAnalysis.Functions.IndicatorSeries.Empty(int).barCount 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Empty\(int\)\.barCount') is negative\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.Enumerator.Current.md b/docs/functions/IndicatorSeries.Enumerator.Current.md new file mode 100644 index 00000000..be78e7e9 --- /dev/null +++ b/docs/functions/IndicatorSeries.Enumerator.Current.md @@ -0,0 +1,24 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries').[Enumerator](IndicatorSeries.Enumerator.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator') + +## IndicatorSeries\.Enumerator\.Current Property + +Gets the current bar and its value\. + +```csharp +public readonly (int Bar,double Value) Current { get; } +``` + +#### Property Value +[<](https://learn.microsoft.com/en-us/dotnet/api/system.valuetuple 'System\.ValueTuple')[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32')[,](https://learn.microsoft.com/en-us/dotnet/api/system.valuetuple 'System\.ValueTuple')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.valuetuple 'System\.ValueTuple') +A pair whose `Bar` is a BAR index in `[FirstBar, LastBar]` and whose +`Value` is the value at that bar\. Valid only after [MoveNext\(\)](IndicatorSeries.Enumerator.MoveNext().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.MoveNext\(\)') has +returned `true`\. + +#### Exceptions + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +[MoveNext\(\)](IndicatorSeries.Enumerator.MoveNext().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.MoveNext\(\)') has not yet been called, or it has already returned + `false`\. `foreach` never reaches this state; hand\-driving the enumerator can, + and a [System\.NullReferenceException](https://learn.microsoft.com/en-us/dotnet/api/system.nullreferenceexception 'System\.NullReferenceException') out of a public API would read as a library + defect rather than as caller misuse\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.Enumerator.MoveNext().md b/docs/functions/IndicatorSeries.Enumerator.MoveNext().md new file mode 100644 index 00000000..47f83823 --- /dev/null +++ b/docs/functions/IndicatorSeries.Enumerator.MoveNext().md @@ -0,0 +1,15 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries').[Enumerator](IndicatorSeries.Enumerator.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator') + +## IndicatorSeries\.Enumerator\.MoveNext\(\) Method + +Advances to the next warm bar\. + +```csharp +public bool MoveNext(); +``` + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when another warm bar is available and [Current](IndicatorSeries.Enumerator.Current.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.Current') has been + positioned on it; `false` when the series is exhausted\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.Enumerator.md b/docs/functions/IndicatorSeries.Enumerator.md new file mode 100644 index 00000000..c58ddb98 --- /dev/null +++ b/docs/functions/IndicatorSeries.Enumerator.md @@ -0,0 +1,30 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.Enumerator Struct + +Enumerates the warm bars of an [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') in ascending BAR order\. + +```csharp +public struct IndicatorSeries.Enumerator +``` + +### Remarks + +Each iteration yields a `(Bar, Value)` pair in which `Bar` is a BAR index into the +source price series β€” an absolute position, never an index into the raw TA-Lib output array. +Bars that have no value are skipped rather than yielded as `null`, so enumerating an +empty series performs zero iterations. + +[System\.Collections\.Generic\.IEnumerable<>](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1 'System\.Collections\.Generic\.IEnumerable\`1') is deliberately not implemented: + `foreach` binds to this pattern directly and allocates nothing, whereas implementing the + interface would box the enumerator on every loop. Adding the interface later is a + non-breaking change; removing an allocating enumerator would not be. + +| Properties | | +| :--- | :--- | +| [Current](IndicatorSeries.Enumerator.Current.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.Current') | Gets the current bar and its value\. | + +| Methods | | +| :--- | :--- | +| [MoveNext\(\)](IndicatorSeries.Enumerator.MoveNext().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator\.MoveNext\(\)') | Advances to the next warm bar\. | diff --git a/docs/functions/IndicatorSeries.Equals.md b/docs/functions/IndicatorSeries.Equals.md new file mode 100644 index 00000000..6438ccad --- /dev/null +++ b/docs/functions/IndicatorSeries.Equals.md @@ -0,0 +1,59 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.Equals Method + +| Overloads | | +| :--- | :--- | +| [Equals\(object\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(object) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(object\)') | Determines whether this series equals the given object\. | +| [Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)') | Determines whether this series equals another\. | + + + +## IndicatorSeries\.Equals\(object\) Method + +Determines whether this series equals the given object\. + +```csharp +public override bool Equals(object? obj); +``` +#### Parameters + + + +`obj` [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') + +The object to compare with\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when [obj](IndicatorSeries.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(object).obj 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(object\)\.obj') is an [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') equal to this + one under [Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)'); otherwise `false`\. + + + +## IndicatorSeries\.Equals\(IndicatorSeries\) Method + +Determines whether this series equals another\. + +```csharp +public bool Equals(TechnicalAnalysis.Functions.IndicatorSeries other); +``` +#### Parameters + + + +`other` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The series to compare with\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when both series share the same backing array \by reference\ and agree on + their first bar, value count, bar count and return code\. + +### Remarks +This does \not\ compare values\. Two series computed separately from identical inputs +are not equal, because they wrap different arrays\. Equality exists so that this value type +satisfies CA1815 and so that [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)') can be recognised as the identity when it +narrows nothing; it is not a numeric comparison\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.FirstBar.md b/docs/functions/IndicatorSeries.FirstBar.md new file mode 100644 index 00000000..519e6d63 --- /dev/null +++ b/docs/functions/IndicatorSeries.FirstBar.md @@ -0,0 +1,16 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.FirstBar Property + +Gets the BAR index of the first bar that has a value, or `null` when no bar has one\. + +```csharp +public System.Nullable FirstBar { get; } +``` + +#### Property Value +[System\.Nullable<](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32')[>](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') +A bar index in `[0, BarCount)` β€” TA\-Lib's `BegIdx`, reinterpreted as the true +lookback of the indicator\. `null` if and only if [HasValues](IndicatorSeries.HasValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.HasValues') is +`false`\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.GetEnumerator().md b/docs/functions/IndicatorSeries.GetEnumerator().md new file mode 100644 index 00000000..80bd323a --- /dev/null +++ b/docs/functions/IndicatorSeries.GetEnumerator().md @@ -0,0 +1,16 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.GetEnumerator\(\) Method + +Returns an allocation\-free enumerator over the bars of this series that carry a value\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries.Enumerator GetEnumerator(); +``` + +#### Returns +[Enumerator](IndicatorSeries.Enumerator.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Enumerator') +An enumerator yielding `(Bar, Value)` pairs in ascending BAR order, one per bar with a +value, where `Bar` is a BAR index in `[FirstBar, LastBar]`\. Bars with no value are +skipped entirely, so enumerating a series with no values performs zero iterations\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.GetHashCode().md b/docs/functions/IndicatorSeries.GetHashCode().md new file mode 100644 index 00000000..385a338b --- /dev/null +++ b/docs/functions/IndicatorSeries.GetHashCode().md @@ -0,0 +1,15 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.GetHashCode\(\) Method + +Returns a hash code consistent with [Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)')\. + +```csharp +public override int GetHashCode(); +``` + +#### Returns +[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') +A hash code derived from the identity of the backing array together with the first bar, +value count, bar count and return code\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.HasValues.md b/docs/functions/IndicatorSeries.HasValues.md new file mode 100644 index 00000000..f3e62fa8 --- /dev/null +++ b/docs/functions/IndicatorSeries.HasValues.md @@ -0,0 +1,17 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.HasValues Property + +Gets a value indicating whether any bar of this series carries a value\. + +```csharp +public bool HasValues { get; } +``` + +#### Property Value +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') is greater than zero\. This is the only warmth test; + a series can carry nothing and still report [TechnicalAnalysis\.Common\.RetCode\.Success](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode.success 'TechnicalAnalysis\.Common\.RetCode\.Success')\. It is + deliberately not called `IsEmpty`: [IsEmpty](PriceSeries.IsEmpty.md 'TechnicalAnalysis\.Functions\.PriceSeries\.IsEmpty') means "no bars", + and a series here can carry no values while covering a hundred bars\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.IsWarmAt(int).md b/docs/functions/IndicatorSeries.IsWarmAt(int).md new file mode 100644 index 00000000..84406c0b --- /dev/null +++ b/docs/functions/IndicatorSeries.IsWarmAt(int).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.IsWarmAt\(int\) Method + +Determines whether the given BAR index has a value\. + +```csharp +public bool IsWarmAt(int bar); +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +A BAR index into the source price series, with domain `[0, BarCount)`\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when [bar](IndicatorSeries.IsWarmAt(int).md#TechnicalAnalysis.Functions.IndicatorSeries.IsWarmAt(int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.IsWarmAt\(int\)\.bar') lies in `[FirstBar, LastBar]`; otherwise + `false`\. Equivalent to `this[bar] is not null`, without the nullable value\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.IsWarmAt(int).md#TechnicalAnalysis.Functions.IndicatorSeries.IsWarmAt(int).bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.IsWarmAt\(int\)\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.LastBar.md b/docs/functions/IndicatorSeries.LastBar.md new file mode 100644 index 00000000..92d6668c --- /dev/null +++ b/docs/functions/IndicatorSeries.LastBar.md @@ -0,0 +1,16 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.LastBar Property + +Gets the BAR index of the last bar that has a value, or `null` when no bar has one\. + +```csharp +public System.Nullable LastBar { get; } +``` + +#### Property Value +[System\.Nullable<](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32')[>](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') +A bar index in `[0, BarCount)`, equal to `FirstBar + WarmCount - 1`\. Note that +this is a bar index and `WarmCount - 1` is an array index; they are different numbers\. +`null` if and only if [HasValues](IndicatorSeries.HasValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.HasValues') is `false`\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.Latest.md b/docs/functions/IndicatorSeries.Latest.md new file mode 100644 index 00000000..b5ec546d --- /dev/null +++ b/docs/functions/IndicatorSeries.Latest.md @@ -0,0 +1,16 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.Latest Property + +Gets the most recent value in the series, or `null` when no bar has a value\. + +```csharp +public System.Nullable Latest { get; } +``` + +#### Property Value +[System\.Nullable<](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') +The value at [LastBar](IndicatorSeries.LastBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.LastBar')\. It is read from \array\ index +`WarmCount - 1`, never from bar index `FirstBar + WarmCount - 1`; the latter would +index the array with a bar index, which is the historical alignment bug\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.RetCode.md b/docs/functions/IndicatorSeries.RetCode.md new file mode 100644 index 00000000..3212589f --- /dev/null +++ b/docs/functions/IndicatorSeries.RetCode.md @@ -0,0 +1,17 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.RetCode Property + +Gets the return code reported by the underlying TA\-Lib call\. + +```csharp +public TechnicalAnalysis.Common.RetCode RetCode { get; } +``` + +#### Property Value +[TechnicalAnalysis\.Common\.RetCode](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode 'TechnicalAnalysis\.Common\.RetCode') +The raw TA\-Lib status\. This answers only "were the parameters acceptable"; it never answers +"are there values"\. A series can report [TechnicalAnalysis\.Common\.RetCode\.Success](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode.success 'TechnicalAnalysis\.Common\.RetCode\.Success') and still hold +nothing, because a period longer than the available data is a success that produces nothing\. +Use [HasValues](IndicatorSeries.HasValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.HasValues') to test for warmth\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.ToBarAlignedArray().md b/docs/functions/IndicatorSeries.ToBarAlignedArray().md new file mode 100644 index 00000000..fd7ad915 --- /dev/null +++ b/docs/functions/IndicatorSeries.ToBarAlignedArray().md @@ -0,0 +1,35 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.ToBarAlignedArray\(\) Method + +Projects the series onto a new array indexed by BAR index, padding bars that have no value +with [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN')\. + +```csharp +public double[] ToBarAlignedArray(); +``` + +#### Returns +[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array') +A new array of exactly [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') elements in which element `i` describes +bar `i`\. The padding value is [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') and is deliberately not +configurable, because padding with `0.0` reinstates exactly the silent corruption this +type exists to prevent\. + +### Remarks + + + is a sentinel here, and it is the one place on this type where + absence is not null. A [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')`[]` cannot hold `null`, so + this projection collapses "this bar has no value" and "this bar has a value and the value is + not finite" onto the same bit pattern. The fluent factories reject non-finite prices, so a + series produced through this API cannot contain a computed [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN'); a + series built through [Create\(RetCode, int, int, double\[\], int\)](IndicatorSeries.Create(RetCode,int,int,double[],int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)') from a hand-rolled call can. When the two states + must be told apart, use [IsWarmAt\(int\)](IndicatorSeries.IsWarmAt(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.IsWarmAt\(int\)'), [this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]') or + [ToBarAlignedNullableArray\(\)](IndicatorSeries.ToBarAlignedNullableArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedNullableArray\(\)'), none of which have a sentinel. + +Because every indicator has a warm-up, the result of this method almost always contains +[System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN'): `ToBarAlignedArray().Max()` and `.Average()` are +[System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') for a typical series, and a chart fed the raw array must be told +how to skip them. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.ToBarAlignedNullableArray().md b/docs/functions/IndicatorSeries.ToBarAlignedNullableArray().md new file mode 100644 index 00000000..b6750a09 --- /dev/null +++ b/docs/functions/IndicatorSeries.ToBarAlignedNullableArray().md @@ -0,0 +1,23 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.ToBarAlignedNullableArray\(\) Method + +Projects the series onto a new array indexed by BAR index, with `null` for bars that +have no value\. + +```csharp +public System.Nullable[] ToBarAlignedNullableArray(); +``` + +#### Returns +[System\.Nullable<](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array') +A new array of exactly [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') elements in which element `i` describes +bar `i`, and a bar with no value is `null` rather than a sentinel\. + +### Remarks +This is the projection that keeps the type's central promise β€” absence is `null` β€” at +the cost of a boxed\-free but larger [System\.Nullable<>](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') array\. Prefer +[ToBarAlignedArray\(\)](IndicatorSeries.ToBarAlignedArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedArray\(\)') only when the consumer needs a contiguous +[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')`[]` and already understands the +[System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') convention\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.WarmCount.md b/docs/functions/IndicatorSeries.WarmCount.md new file mode 100644 index 00000000..a26d197a --- /dev/null +++ b/docs/functions/IndicatorSeries.WarmCount.md @@ -0,0 +1,20 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.WarmCount Property + +Gets the number of bars that carry a value β€” TA\-Lib's `NBElement`\. + +```csharp +public int WarmCount { get; } +``` + +#### Property Value +[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') +A count in the range `[0, BarCount]`, expressed in ARRAY space: it is the length of +[WarmValues](IndicatorSeries.WarmValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValues')\. It is deliberately \not\ called `Count`, because on a +type with an indexer `Count` reads as "the number of valid indices" and this is not +that: the indexer's domain is `[0, BarCount)`\. Looping `for (int i = 0; i < + s.WarmCount; i++) s[i]` would read the wrong bars and silently drop the most recent +ones\. The last bar that carries a value is [LastBar](IndicatorSeries.LastBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.LastBar'), which is +`FirstBar + WarmCount - 1`\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.WarmValues.md b/docs/functions/IndicatorSeries.WarmValues.md new file mode 100644 index 00000000..fa6429dc --- /dev/null +++ b/docs/functions/IndicatorSeries.WarmValues.md @@ -0,0 +1,20 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.WarmValues Property + +Gets the values as a span whose element `k` describes bar `FirstBar + k`\. + +```csharp +public System.ReadOnlySpan WarmValues { get; } +``` + +#### Property Value +[System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') +A read\-only span of exactly [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') elements β€” the single ARRAY\-indexed view +on this type, which is why it is named `WarmValues` rather than `Values`\. It +starts at array index `0` and is sliced to [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount'), so TA\-Lib's +untouched zero padding is unreachable and the historical expression +`Values[BegIdx + NBElement - 1]` throws [System\.IndexOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.indexoutofrangeexception 'System\.IndexOutOfRangeException') instead +of silently returning a padding zero\. A bar index is \not\ a valid subscript here: use +[this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]') for that\. A series with no values yields an empty span\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.WarmValuesToArray().md b/docs/functions/IndicatorSeries.WarmValuesToArray().md new file mode 100644 index 00000000..36939a32 --- /dev/null +++ b/docs/functions/IndicatorSeries.WarmValuesToArray().md @@ -0,0 +1,18 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.WarmValuesToArray\(\) Method + +Copies the values into a new array whose element `k` describes bar +`FirstBar + k`\. + +```csharp +public double[] WarmValuesToArray(); +``` + +#### Returns +[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array') +A new array of exactly [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') elements, ARRAY\-indexed exactly as +[WarmValues](IndicatorSeries.WarmValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValues') is\. It exists because [WarmValues](IndicatorSeries.WarmValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValues') is a +[System\.ReadOnlySpan<>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') and therefore cannot escape into a LINQ query, an +`async` method or a field\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.md b/docs/functions/IndicatorSeries.md new file mode 100644 index 00000000..895757c2 --- /dev/null +++ b/docs/functions/IndicatorSeries.md @@ -0,0 +1,122 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## IndicatorSeries Struct + +A bar\-aligned view over a single output series produced by a TA\-Lib indicator\. +This is the one place in the library where TA\-Lib's raw alignment metadata is interpreted\. + +```csharp +public readonly struct IndicatorSeries : System.IEquatable +``` + +Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') + +### Example + +```csharp +PriceSeries prices = PriceSeries.FromClose(closes); +IndicatorSeries sma = prices.Sma(30); + +double? latest = sma.Latest; // null until the indicator has warmed up +double? atBar50 = sma[50]; // 50 is a BAR index, not an array index +if (sma[50] is { } value) +{ + Console.WriteLine(value); +} +``` + +### Remarks + +There are two index spaces, and confusing them is the classic TA-Lib bug. + +An array index is a subscript into the raw [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')`[]` that a +`TAMath` call returned. TA-Lib fills that array starting at array index `0`, and it +writes exactly `NBElement` elements, so the last valid array index is +`NBElement - 1`. Everything from `NBElement` to the end of the allocated array is +untouched zero padding and means nothing. + +A bar index is a position in the source price series, with domain +`[0, BarCount)`. TA-Lib reports `BegIdx`, which is a bar index: output array +element `k` describes bar `BegIdx + k`. The last valid bar index is therefore +`BegIdx + NBElement - 1`. + +`NBElement - 1` and `BegIdx + NBElement - 1` are two different numbers; they coincide + only when `BegIdx == 0`. Subscripting the output array with the bar index + `BegIdx + NBElement - 1` lands in the zero padding and silently yields `0.0`. That is + the mistake this type exists to make unsayable. + +The rule. Every [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') on this type that names a position is a BAR index. + That covers the `bar` parameter of [this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]'), [IsWarmAt\(int\)](IndicatorSeries.IsWarmAt(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.IsWarmAt\(int\)'), + [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)') and all four crossing overloads; the values returned by + [FirstBar](IndicatorSeries.FirstBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.FirstBar') and [LastBar](IndicatorSeries.LastBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.LastBar'); the `Bar` component yielded by + [GetEnumerator\(\)](IndicatorSeries.GetEnumerator().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.GetEnumerator\(\)'); and the index space of [ToBarAlignedArray\(\)](IndicatorSeries.ToBarAlignedArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedArray\(\)'), + [ToBarAlignedNullableArray\(\)](IndicatorSeries.ToBarAlignedNullableArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedNullableArray\(\)') and [CopyBarAligned\(Span<double>\)](IndicatorSeries.CopyBarAligned(Span_double_).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CopyBarAligned\(System\.Span\\)'). + [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') and [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') are counts, not indices, and neither is a + valid loop bound for the indexer β€” the indexer's domain is `[0, BarCount)`, so a loop over + it must be bounded by [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount'). No member of this type accepts or returns a raw + TA-Lib output-array index, with the single deliberate exception of [WarmValues](IndicatorSeries.WarmValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValues'), + whose name and documentation state its index space explicitly. [Create\(RetCode, int, int, double\[\], int\)](IndicatorSeries.Create(RetCode,int,int,double[],int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)') is the sole + point at which raw TA-Lib metadata enters the type system. + +Absence is null, uniformly. A bar that is inside the series but before the + indicator has warmed up has no value, and that is reported as `null` β€” never `0.0`, + never [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN'), and never an exception. There is no sentinel value anywhere on + this type: [this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]') and [Latest](IndicatorSeries.Latest.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Latest') are [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')`?`, + and [FirstBar](IndicatorSeries.FirstBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.FirstBar') and [LastBar](IndicatorSeries.LastBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.LastBar') are [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32')`?`. The single + exception is [ToBarAlignedArray\(\)](IndicatorSeries.ToBarAlignedArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedArray\(\)') and [CopyBarAligned\(Span<double>\)](IndicatorSeries.CopyBarAligned(Span_double_).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CopyBarAligned\(System\.Span\\)'), which pad with + [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') because a [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')`[]` cannot hold `null`; + [ToBarAlignedNullableArray\(\)](IndicatorSeries.ToBarAlignedNullableArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedNullableArray\(\)') is the projection that keeps the promise. + +A bar outside [0, BarCount) is a caller bug and throws + [System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException'). "Bar 5 of a 30-period SMA" is a legitimate question + whose answer is "no value"; "bar 5000 of a 100-bar series" is not a question at all. That split + is what turns [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)') into a causality guarantee rather than a convention. + + + is not warmth. A successful call over too little data reports + [TechnicalAnalysis\.Common\.RetCode\.Success](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode.success 'TechnicalAnalysis\.Common\.RetCode\.Success') with `BegIdx == 0` and `NBElement == 0`, and in + that state `BegIdx` is a lie. [HasValues](IndicatorSeries.HasValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.HasValues') (equivalently + `WarmCount > 0`) is the only warmth test. `default(IndicatorSeries)` is a valid + series with no values that reports [TechnicalAnalysis\.Common\.RetCode\.Success](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.retcode.success 'TechnicalAnalysis\.Common\.RetCode\.Success'), because + `Success == 0`. + +This is an immutable value type, and its immutability is unconditional: [Create\(RetCode, int, int, double\[\], int\)](IndicatorSeries.Create(RetCode,int,int,double[],int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)') +copies the warm values out of the array it is handed, so no caller can reach inside a series +after handing it over. Every instance is therefore safe for unrestricted concurrent use. + +| Properties | | +| :--- | :--- | +| [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount') | Gets the number of bars in the source price series\. | +| [FirstBar](IndicatorSeries.FirstBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.FirstBar') | Gets the BAR index of the first bar that has a value, or `null` when no bar has one\. | +| [HasValues](IndicatorSeries.HasValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.HasValues') | Gets a value indicating whether any bar of this series carries a value\. | +| [LastBar](IndicatorSeries.LastBar.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.LastBar') | Gets the BAR index of the last bar that has a value, or `null` when no bar has one\. | +| [Latest](IndicatorSeries.Latest.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Latest') | Gets the most recent value in the series, or `null` when no bar has a value\. | +| [RetCode](IndicatorSeries.RetCode.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.RetCode') | Gets the return code reported by the underlying TA\-Lib call\. | +| [this\[int\]](IndicatorSeries.this[int].md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]') | Gets the value at the given BAR index, or `null` when that bar has no value\. | +| [WarmCount](IndicatorSeries.WarmCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmCount') | Gets the number of bars that carry a value β€” TA\-Lib's `NBElement`\. | +| [WarmValues](IndicatorSeries.WarmValues.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValues') | Gets the values as a span whose element `k` describes bar `FirstBar + k`\. | + +| Methods | | +| :--- | :--- | +| [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)') | Returns the same series truncated so that it ends at the given BAR index, making look\-ahead unrepresentable rather than merely detectable\. | +| [CopyBarAligned\(Span<double>\)](IndicatorSeries.CopyBarAligned(Span_double_).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CopyBarAligned\(System\.Span\\)') | Writes the series into the given span in which the index is the BAR index\. | +| [Create\(RetCode, int, int, double\[\], int\)](IndicatorSeries.Create(RetCode,int,int,double[],int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Create\(TechnicalAnalysis\.Common\.RetCode, int, int, double\[\], int\)') | Creates a bar\-aligned series from the raw metadata of a TA\-Lib call\. This is the only point in the library at which raw TA\-Lib alignment metadata enters the type system\. | +| [CrossedAbove\(double, int\)](IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(double, int\)') | Determines whether this series crossed above a fixed level at the given BAR index\. | +| [CrossedAbove\(IndicatorSeries, int\)](IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedAbove\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)') | Determines whether this series crossed above another series at the given BAR index\. | +| [CrossedBelow\(double, int\)](IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(double, int\)') | Determines whether this series crossed below a fixed level at the given BAR index\. | +| [CrossedBelow\(IndicatorSeries, int\)](IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.CrossedBelow\(TechnicalAnalysis\.Functions\.IndicatorSeries, int\)') | Determines whether this series crossed below another series at the given BAR index\. | +| [Empty\(int\)](IndicatorSeries.Empty(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Empty\(int\)') | Creates a series covering the given number of bars in which no bar has a value\. | +| [Equals\(object\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(object) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(object\)') | Determines whether this series equals the given object\. | +| [Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)') | Determines whether this series equals another\. | +| [GetEnumerator\(\)](IndicatorSeries.GetEnumerator().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.GetEnumerator\(\)') | Returns an allocation\-free enumerator over the bars of this series that carry a value\. | +| [GetHashCode\(\)](IndicatorSeries.GetHashCode().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.GetHashCode\(\)') | Returns a hash code consistent with [Equals\(IndicatorSeries\)](IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries) 'TechnicalAnalysis\.Functions\.IndicatorSeries\.Equals\(TechnicalAnalysis\.Functions\.IndicatorSeries\)')\. | +| [IsWarmAt\(int\)](IndicatorSeries.IsWarmAt(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.IsWarmAt\(int\)') | Determines whether the given BAR index has a value\. | +| [ToBarAlignedArray\(\)](IndicatorSeries.ToBarAlignedArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedArray\(\)') | Projects the series onto a new array indexed by BAR index, padding bars that have no value with [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN')\. | +| [ToBarAlignedNullableArray\(\)](IndicatorSeries.ToBarAlignedNullableArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.ToBarAlignedNullableArray\(\)') | Projects the series onto a new array indexed by BAR index, with `null` for bars that have no value\. | +| [WarmValuesToArray\(\)](IndicatorSeries.WarmValuesToArray().md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.WarmValuesToArray\(\)') | Copies the values into a new array whose element `k` describes bar `FirstBar + k`\. | + +| Operators | | +| :--- | :--- | +| [operator ==\(IndicatorSeries, IndicatorSeries\)](IndicatorSeries.operator(IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.op\_Equality\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)') | Determines whether two series are equal\. | +| [operator \!=\(IndicatorSeries, IndicatorSeries\)](IndicatorSeries.operator!(IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.op\_Inequality\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)') | Determines whether two series are not equal\. | diff --git a/docs/functions/IndicatorSeries.operator!(IndicatorSeries,IndicatorSeries).md b/docs/functions/IndicatorSeries.operator!(IndicatorSeries,IndicatorSeries).md new file mode 100644 index 00000000..785e5103 --- /dev/null +++ b/docs/functions/IndicatorSeries.operator!(IndicatorSeries,IndicatorSeries).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.operator \!=\(IndicatorSeries, IndicatorSeries\) Operator + +Determines whether two series are not equal\. + +```csharp +public static bool operator !=(TechnicalAnalysis.Functions.IndicatorSeries left, TechnicalAnalysis.Functions.IndicatorSeries right); +``` +#### Parameters + + + +`left` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The first series\. + + + +`right` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The second series\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the two series are not equal; otherwise `false`\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.operator(IndicatorSeries,IndicatorSeries).md b/docs/functions/IndicatorSeries.operator(IndicatorSeries,IndicatorSeries).md new file mode 100644 index 00000000..dcadafea --- /dev/null +++ b/docs/functions/IndicatorSeries.operator(IndicatorSeries,IndicatorSeries).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.operator ==\(IndicatorSeries, IndicatorSeries\) Operator + +Determines whether two series are equal\. + +```csharp +public static bool operator ==(TechnicalAnalysis.Functions.IndicatorSeries left, TechnicalAnalysis.Functions.IndicatorSeries right); +``` +#### Parameters + + + +`left` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The first series\. + + + +`right` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The second series\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the two series are equal; otherwise `false`\. \ No newline at end of file diff --git a/docs/functions/IndicatorSeries.this[int].md b/docs/functions/IndicatorSeries.this[int].md new file mode 100644 index 00000000..dc25ae76 --- /dev/null +++ b/docs/functions/IndicatorSeries.this[int].md @@ -0,0 +1,32 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +## IndicatorSeries\.this\[int\] Property + +Gets the value at the given BAR index, or `null` when that bar has no value\. + +```csharp +public System.Nullable this[int bar] { get; } +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +A BAR index into the source price series, with domain `[0, BarCount)`\. This is +\not\ an index into the raw TA\-Lib output array\. + +#### Property Value +[System\.Nullable<](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.nullable-1 'System\.Nullable\`1') +The value describing [bar](IndicatorSeries.this[int].md#TechnicalAnalysis.Functions.IndicatorSeries.this[int].bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]\.bar'), or `null` when [bar](IndicatorSeries.this[int].md#TechnicalAnalysis.Functions.IndicatorSeries.this[int].bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]\.bar') is +inside the series but outside `[FirstBar, LastBar]` β€” typically a bar before the +indicator warmed up\. Never `0.0` and never [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') for a bar that has +no value\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](IndicatorSeries.this[int].md#TechnicalAnalysis.Functions.IndicatorSeries.this[int].bar 'TechnicalAnalysis\.Functions\.IndicatorSeries\.this\[int\]\.bar') is negative or greater than or equal to [BarCount](IndicatorSeries.BarCount.md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.BarCount')\. + Asking about a bar the series does not cover is a caller bug, whereas asking about a bar + that has not warmed up is a legitimate question answered with `null`\. \ No newline at end of file diff --git a/docs/functions/KamaResult.md b/docs/functions/KamaResult.md index 88a1789b..9ed770e0 100644 --- a/docs/functions/KamaResult.md +++ b/docs/functions/KamaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Kaufman Adaptive Moving Average \(KAMA\ public record KamaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 KamaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ KamaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[KamaResult](KamaResult.md 'TechnicalAnalysis\.Functions\.KamaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/LinearRegAngleResult.md b/docs/functions/LinearRegAngleResult.md index dc3af09d..0848eed8 100644 --- a/docs/functions/LinearRegAngleResult.md +++ b/docs/functions/LinearRegAngleResult.md @@ -11,7 +11,7 @@ into the strength and direction of the trend over a specified period\. public record LinearRegAngleResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 LinearRegAngleResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ LinearRegAngleResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[LinearRegAngleResult](LinearRegAngleResult.md 'TechnicalAnalysis\.Functions\.LinearRegAngleResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/LinearRegInterceptResult.md b/docs/functions/LinearRegInterceptResult.md index f61cfe7c..5186550d 100644 --- a/docs/functions/LinearRegInterceptResult.md +++ b/docs/functions/LinearRegInterceptResult.md @@ -11,7 +11,7 @@ the regression line would cross the y\-axis if extended backward\. public record LinearRegInterceptResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 LinearRegInterceptResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ LinearRegInterceptResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[LinearRegInterceptResult](LinearRegInterceptResult.md 'TechnicalAnalysis\.Functions\.LinearRegInterceptResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/LinearRegResult.md b/docs/functions/LinearRegResult.md index 059d7e15..949b0d0f 100644 --- a/docs/functions/LinearRegResult.md +++ b/docs/functions/LinearRegResult.md @@ -11,7 +11,7 @@ best\-fit line through the price data over a specified period\. public record LinearRegResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 LinearRegResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ LinearRegResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[LinearRegResult](LinearRegResult.md 'TechnicalAnalysis\.Functions\.LinearRegResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/LinearRegSlopeResult.md b/docs/functions/LinearRegSlopeResult.md index eaeaeaf8..7bf8eee6 100644 --- a/docs/functions/LinearRegSlopeResult.md +++ b/docs/functions/LinearRegSlopeResult.md @@ -11,7 +11,7 @@ in price over the specified period\. public record LinearRegSlopeResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 LinearRegSlopeResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ LinearRegSlopeResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[LinearRegSlopeResult](LinearRegSlopeResult.md 'TechnicalAnalysis\.Functions\.LinearRegSlopeResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/LnResult.md b/docs/functions/LnResult.md index 7a773378..6abcbd55 100644 --- a/docs/functions/LnResult.md +++ b/docs/functions/LnResult.md @@ -9,7 +9,7 @@ Represents the result of the vector natural logarithm operation \(LN function\)\ public record LnResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 LnResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ LnResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[LnResult](LnResult.md 'TechnicalAnalysis\.Functions\.LnResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/Log10Result.md b/docs/functions/Log10Result.md index f57aee0e..4a7b310e 100644 --- a/docs/functions/Log10Result.md +++ b/docs/functions/Log10Result.md @@ -9,7 +9,7 @@ Represents the result of the vector base\-10 logarithm operation \(LOG10 functio public record Log10Result : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 Log10Result +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ Log10Result Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[Log10Result](Log10Result.md 'TechnicalAnalysis\.Functions\.Log10Result')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MacdExtResult.md b/docs/functions/MacdExtResult.md index 9b03a560..ff53772e 100644 --- a/docs/functions/MacdExtResult.md +++ b/docs/functions/MacdExtResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Extended MACD indicator with configurab public record MacdExtResult : TechnicalAnalysis.Common.TripleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') 🡒 MacdExtResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') β†’ MacdExtResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MacdExtResult](MacdExtResult.md 'TechnicalAnalysis\.Functions\.MacdExtResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MacdFixResult.md b/docs/functions/MacdFixResult.md index b1b4ba2b..628a0ae2 100644 --- a/docs/functions/MacdFixResult.md +++ b/docs/functions/MacdFixResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the MACD Fix indicator with fixed 12/26 per public record MacdFixResult : TechnicalAnalysis.Common.TripleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') 🡒 MacdFixResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') β†’ MacdFixResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MacdFixResult](MacdFixResult.md 'TechnicalAnalysis\.Functions\.MacdFixResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MacdResult.md b/docs/functions/MacdResult.md index 8fe962dc..e8027bc7 100644 --- a/docs/functions/MacdResult.md +++ b/docs/functions/MacdResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Moving Average Convergence Divergence \ public record MacdResult : TechnicalAnalysis.Common.TripleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') 🡒 MacdResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.TripleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.tripleoutputresult 'TechnicalAnalysis\.Common\.TripleOutputResult') β†’ MacdResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MacdResult](MacdResult.md 'TechnicalAnalysis\.Functions\.MacdResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MacdSeries.AsOf(int).md b/docs/functions/MacdSeries.AsOf(int).md new file mode 100644 index 00000000..e8977745 --- /dev/null +++ b/docs/functions/MacdSeries.AsOf(int).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') + +## MacdSeries\.AsOf\(int\) Method + +Narrows every component so that it ends at the given BAR index\. + +```csharp +public TechnicalAnalysis.Functions.MacdSeries AsOf(int bar); +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The last BAR index the narrowed components are allowed to know about, with domain +`[0, BarCount)` of the components\. + +#### Returns +[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') +A result whose three components have each been narrowed by [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)')\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](MacdSeries.AsOf(int).md#TechnicalAnalysis.Functions.MacdSeries.AsOf(int).bar 'TechnicalAnalysis\.Functions\.MacdSeries\.AsOf\(int\)\.bar') is outside the bars the components cover\. \ No newline at end of file diff --git a/docs/functions/MacdSeries.Histogram.md b/docs/functions/MacdSeries.Histogram.md new file mode 100644 index 00000000..8cf010cf --- /dev/null +++ b/docs/functions/MacdSeries.Histogram.md @@ -0,0 +1,13 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') + +## MacdSeries\.Histogram Property + +The histogram: Line minus Signal\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Histogram { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/MacdSeries.Line.md b/docs/functions/MacdSeries.Line.md new file mode 100644 index 00000000..3c614f9b --- /dev/null +++ b/docs/functions/MacdSeries.Line.md @@ -0,0 +1,13 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') + +## MacdSeries\.Line Property + +The MACD line: the fast exponential moving average minus the slow one\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Line { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md b/docs/functions/MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md new file mode 100644 index 00000000..c8416824 --- /dev/null +++ b/docs/functions/MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md @@ -0,0 +1,34 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') + +## MacdSeries\(IndicatorSeries, IndicatorSeries, IndicatorSeries\) Constructor + +The three bar\-aligned outputs of a MACD calculation\. + +```csharp +public MacdSeries(TechnicalAnalysis.Functions.IndicatorSeries Line, TechnicalAnalysis.Functions.IndicatorSeries Signal, TechnicalAnalysis.Functions.IndicatorSeries Histogram); +``` +#### Parameters + + + +`Line` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The MACD line: the fast exponential moving average minus the slow one\. Bar\-aligned\. + + + +`Signal` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The signal line: an exponential moving average of [Line](MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md#TechnicalAnalysis.Functions.MacdSeries.MacdSeries(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries).Line 'TechnicalAnalysis\.Functions\.MacdSeries\.MacdSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)\.Line')\. Bar\-aligned\. + + + +`Histogram` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The histogram: [Line](MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md#TechnicalAnalysis.Functions.MacdSeries.MacdSeries(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries).Line 'TechnicalAnalysis\.Functions\.MacdSeries\.MacdSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)\.Line') minus [Signal](MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md#TechnicalAnalysis.Functions.MacdSeries.MacdSeries(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries).Signal 'TechnicalAnalysis\.Functions\.MacdSeries\.MacdSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)\.Signal')\. Bar\-aligned\. + +### Remarks +Each component is an independently addressable [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries'), so a signal\-line +crossing needs no dedicated member: `macd.Line.CrossedAbove(macd.Signal, bar)` already says +it, in bar indices, with the differing warm\-ups handled for you\. \ No newline at end of file diff --git a/docs/functions/MacdSeries.Signal.md b/docs/functions/MacdSeries.Signal.md new file mode 100644 index 00000000..b580b2c9 --- /dev/null +++ b/docs/functions/MacdSeries.Signal.md @@ -0,0 +1,13 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') + +## MacdSeries\.Signal Property + +The signal line: an exponential moving average of Line\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Signal { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/MacdSeries.md b/docs/functions/MacdSeries.md new file mode 100644 index 00000000..70037399 --- /dev/null +++ b/docs/functions/MacdSeries.md @@ -0,0 +1,31 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## MacdSeries Struct + +The three bar\-aligned outputs of a MACD calculation\. + +```csharp +public readonly record struct MacdSeries : System.IEquatable +``` + +Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') + +### Remarks +Each component is an independently addressable [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries'), so a signal\-line +crossing needs no dedicated member: `macd.Line.CrossedAbove(macd.Signal, bar)` already says +it, in bar indices, with the differing warm\-ups handled for you\. + +| Constructors | | +| :--- | :--- | +| [MacdSeries\(IndicatorSeries, IndicatorSeries, IndicatorSeries\)](MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.MacdSeries\.MacdSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)') | The three bar\-aligned outputs of a MACD calculation\. | + +| Properties | | +| :--- | :--- | +| [Histogram](MacdSeries.Histogram.md 'TechnicalAnalysis\.Functions\.MacdSeries\.Histogram') | The histogram: Line minus Signal\. Bar\-aligned\. | +| [Line](MacdSeries.Line.md 'TechnicalAnalysis\.Functions\.MacdSeries\.Line') | The MACD line: the fast exponential moving average minus the slow one\. Bar\-aligned\. | +| [Signal](MacdSeries.Signal.md 'TechnicalAnalysis\.Functions\.MacdSeries\.Signal') | The signal line: an exponential moving average of Line\. Bar\-aligned\. | + +| Methods | | +| :--- | :--- | +| [AsOf\(int\)](MacdSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.MacdSeries\.AsOf\(int\)') | Narrows every component so that it ends at the given BAR index\. | diff --git a/docs/functions/MamaResult.md b/docs/functions/MamaResult.md index e832ffe9..7550bef7 100644 --- a/docs/functions/MamaResult.md +++ b/docs/functions/MamaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the MESA Adaptive Moving Average \(MAMA\) i public record MamaResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 MamaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ MamaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MamaResult](MamaResult.md 'TechnicalAnalysis\.Functions\.MamaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MaxIndexResult.md b/docs/functions/MaxIndexResult.md index 0628f888..0f5d9f1b 100644 --- a/docs/functions/MaxIndexResult.md +++ b/docs/functions/MaxIndexResult.md @@ -10,7 +10,7 @@ The MAXINDEX function identifies the position \(index\) of the maximum value wit public record MaxIndexResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 MaxIndexResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ MaxIndexResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MaxIndexResult](MaxIndexResult.md 'TechnicalAnalysis\.Functions\.MaxIndexResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MaxResult.md b/docs/functions/MaxResult.md index 3886b743..9c07c85e 100644 --- a/docs/functions/MaxResult.md +++ b/docs/functions/MaxResult.md @@ -10,7 +10,7 @@ The MAX function identifies the maximum value within a rolling window of data po public record MaxResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MaxResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MaxResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MaxResult](MaxResult.md 'TechnicalAnalysis\.Functions\.MaxResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MedPriceResult.md b/docs/functions/MedPriceResult.md index 2f377f7c..f171ee2e 100644 --- a/docs/functions/MedPriceResult.md +++ b/docs/functions/MedPriceResult.md @@ -9,7 +9,7 @@ Represents the result of the Median Price calculation\. public record MedPriceResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MedPriceResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MedPriceResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MedPriceResult](MedPriceResult.md 'TechnicalAnalysis\.Functions\.MedPriceResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MfiResult.md b/docs/functions/MfiResult.md index 680c41ee..e62ea8fb 100644 --- a/docs/functions/MfiResult.md +++ b/docs/functions/MfiResult.md @@ -9,7 +9,7 @@ Represents the result of the Money Flow Index \(MFI\) indicator calculation\. public record MfiResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MfiResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MfiResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MfiResult](MfiResult.md 'TechnicalAnalysis\.Functions\.MfiResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MidPointResult.md b/docs/functions/MidPointResult.md index de2ce0aa..123e8f63 100644 --- a/docs/functions/MidPointResult.md +++ b/docs/functions/MidPointResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the MidPoint indicator\. public record MidPointResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MidPointResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MidPointResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MidPointResult](MidPointResult.md 'TechnicalAnalysis\.Functions\.MidPointResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MidPriceResult.md b/docs/functions/MidPriceResult.md index 3e0e6426..853c2f75 100644 --- a/docs/functions/MidPriceResult.md +++ b/docs/functions/MidPriceResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the MidPrice indicator\. public record MidPriceResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MidPriceResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MidPriceResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MidPriceResult](MidPriceResult.md 'TechnicalAnalysis\.Functions\.MidPriceResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MinIndexResult.md b/docs/functions/MinIndexResult.md index 26695b86..62b3acbb 100644 --- a/docs/functions/MinIndexResult.md +++ b/docs/functions/MinIndexResult.md @@ -10,7 +10,7 @@ The MININDEX function identifies the position \(index\) of the minimum value wit public record MinIndexResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 MinIndexResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ MinIndexResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MinIndexResult](MinIndexResult.md 'TechnicalAnalysis\.Functions\.MinIndexResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MinMaxIndexResult.md b/docs/functions/MinMaxIndexResult.md index 13471797..55066db7 100644 --- a/docs/functions/MinMaxIndexResult.md +++ b/docs/functions/MinMaxIndexResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the indices of minimum and maximum values o public record MinMaxIndexResult : TechnicalAnalysis.Common.IndicatorResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 MinMaxIndexResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ MinMaxIndexResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MinMaxIndexResult](MinMaxIndexResult.md 'TechnicalAnalysis\.Functions\.MinMaxIndexResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MinMaxResult.md b/docs/functions/MinMaxResult.md index 7424313e..338c4058 100644 --- a/docs/functions/MinMaxResult.md +++ b/docs/functions/MinMaxResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the minimum and maximum values over a speci public record MinMaxResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 MinMaxResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ MinMaxResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MinMaxResult](MinMaxResult.md 'TechnicalAnalysis\.Functions\.MinMaxResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MinResult.md b/docs/functions/MinResult.md index a44d1537..9fddedb3 100644 --- a/docs/functions/MinResult.md +++ b/docs/functions/MinResult.md @@ -10,7 +10,7 @@ The MIN function identifies the minimum value within a rolling window of data po public record MinResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MinResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MinResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MinResult](MinResult.md 'TechnicalAnalysis\.Functions\.MinResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MinusDIResult.md b/docs/functions/MinusDIResult.md index d4103114..51f8a119 100644 --- a/docs/functions/MinusDIResult.md +++ b/docs/functions/MinusDIResult.md @@ -10,7 +10,7 @@ Represents the result of the Minus Directional Indicator \(\-DI\) calculation\. public record MinusDIResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MinusDIResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MinusDIResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MinusDIResult](MinusDIResult.md 'TechnicalAnalysis\.Functions\.MinusDIResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MinusDMResult.md b/docs/functions/MinusDMResult.md index ec744c91..13cfb5dc 100644 --- a/docs/functions/MinusDMResult.md +++ b/docs/functions/MinusDMResult.md @@ -10,7 +10,7 @@ Represents the result of the Minus Directional Movement \(\-DM\) indicator calcu public record MinusDMResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MinusDMResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MinusDMResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MinusDMResult](MinusDMResult.md 'TechnicalAnalysis\.Functions\.MinusDMResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MomResult.md b/docs/functions/MomResult.md index 8f19904b..44fdef8e 100644 --- a/docs/functions/MomResult.md +++ b/docs/functions/MomResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Momentum \(MOM\) indicator\. public record MomResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MomResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MomResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MomResult](MomResult.md 'TechnicalAnalysis\.Functions\.MomResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MomentumIndicators.Adx(thisPriceSeries,int).md b/docs/functions/MomentumIndicators.Adx(thisPriceSeries,int).md new file mode 100644 index 00000000..0f66301e --- /dev/null +++ b/docs/functions/MomentumIndicators.Adx(thisPriceSeries,int).md @@ -0,0 +1,40 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MomentumIndicators](MomentumIndicators.md 'TechnicalAnalysis\.Functions\.MomentumIndicators') + +## MomentumIndicators\.Adx\(this PriceSeries, int\) Method + +Computes the average directional index β€” the strength of a trend, without its direction\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Adx(this TechnicalAnalysis.Functions.PriceSeries prices, int timePeriod=14); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. It must carry high and low prices, because the directional movement of a +bar is defined by how its range extends beyond the previous bar's\. + + + +`timePeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The Wilder smoothing period\. Defaults to 14, as in the raw layer\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A bar\-aligned series of values from 0 to 100\. The first bar is +`(2 * timePeriod) + unstablePeriod - 1`, which is 27 for the default period β€” the +longest warm\-up of any indicator on this surface, because the index is a smoothed average of +a smoothed average\. A value says only how strongly price is trending; whether it is trending +up or down is what `TAMath.PlusDI` and `TAMath.MinusDI` answer\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[timePeriod](MomentumIndicators.Adx(thisPriceSeries,int).md#TechnicalAnalysis.Functions.MomentumIndicators.Adx(thisTechnicalAnalysis.Functions.PriceSeries,int).timePeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Adx\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)\.timePeriod') is outside 2 to 100000\. + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The price series carries no high and low prices\. \ No newline at end of file diff --git a/docs/functions/MomentumIndicators.Macd(thisPriceSeries,int,int,int).md b/docs/functions/MomentumIndicators.Macd(thisPriceSeries,int,int,int).md new file mode 100644 index 00000000..7b2ce96f --- /dev/null +++ b/docs/functions/MomentumIndicators.Macd(thisPriceSeries,int,int,int).md @@ -0,0 +1,48 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MomentumIndicators](MomentumIndicators.md 'TechnicalAnalysis\.Functions\.MomentumIndicators') + +## MomentumIndicators\.Macd\(this PriceSeries, int, int, int\) Method + +Computes the moving average convergence divergence of the closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.MacdSeries Macd(this TechnicalAnalysis.Functions.PriceSeries prices, int fastPeriod=12, int slowPeriod=26, int signalPeriod=9); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. + + + +`fastPeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The fast exponential moving average period\. Defaults to 12, as in the raw layer\. + + + +`slowPeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The slow exponential moving average period\. Defaults to 26, as in the raw layer\. + + + +`signalPeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The period of the signal line, an exponential moving average of the MACD line\. Defaults to 9, +as in the raw layer\. + +#### Returns +[MacdSeries](MacdSeries.md 'TechnicalAnalysis\.Functions\.MacdSeries') +The line, signal and histogram, each independently bar\-aligned and sharing the same first +bar\. A signal crossing is `macd.Line.CrossedAbove(macd.Signal, bar)`\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[fastPeriod](MomentumIndicators.Macd(thisPriceSeries,int,int,int).md#TechnicalAnalysis.Functions.MomentumIndicators.Macd(thisTechnicalAnalysis.Functions.PriceSeries,int,int,int).fastPeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Macd\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, int\)\.fastPeriod'), [slowPeriod](MomentumIndicators.Macd(thisPriceSeries,int,int,int).md#TechnicalAnalysis.Functions.MomentumIndicators.Macd(thisTechnicalAnalysis.Functions.PriceSeries,int,int,int).slowPeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Macd\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, int\)\.slowPeriod') or + [signalPeriod](MomentumIndicators.Macd(thisPriceSeries,int,int,int).md#TechnicalAnalysis.Functions.MomentumIndicators.Macd(thisTechnicalAnalysis.Functions.PriceSeries,int,int,int).signalPeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Macd\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, int\)\.signalPeriod') is outside 2 to 100000\. The signal period matters most: the + raw layer does not reject a signal period of 1, it fails inside an internal array copy\. \ No newline at end of file diff --git a/docs/functions/MomentumIndicators.Rsi(thisPriceSeries,int).md b/docs/functions/MomentumIndicators.Rsi(thisPriceSeries,int).md new file mode 100644 index 00000000..060c9050 --- /dev/null +++ b/docs/functions/MomentumIndicators.Rsi(thisPriceSeries,int).md @@ -0,0 +1,35 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MomentumIndicators](MomentumIndicators.md 'TechnicalAnalysis\.Functions\.MomentumIndicators') + +## MomentumIndicators\.Rsi\(this PriceSeries, int\) Method + +Computes the relative strength index of the closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Rsi(this TechnicalAnalysis.Functions.PriceSeries prices, int timePeriod=14); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. + + + +`timePeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The lookback period\. Defaults to 14, as in the raw layer\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A bar\-aligned series of values from 0 to 100\. A strictly rising series is pinned at 100 and a +flat series reads 0, matching the reference C implementation\. If the series holds fewer bars +than the period needs, the result is empty and reports success β€” and an empty result answers +`null` rather than `0`, so it can never be mistaken for an oversold reading\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[timePeriod](MomentumIndicators.Rsi(thisPriceSeries,int).md#TechnicalAnalysis.Functions.MomentumIndicators.Rsi(thisTechnicalAnalysis.Functions.PriceSeries,int).timePeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Rsi\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)\.timePeriod') is outside 2 to 100000\. \ No newline at end of file diff --git a/docs/functions/MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md b/docs/functions/MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md new file mode 100644 index 00000000..b2b60ca8 --- /dev/null +++ b/docs/functions/MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md @@ -0,0 +1,61 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[MomentumIndicators](MomentumIndicators.md 'TechnicalAnalysis\.Functions\.MomentumIndicators') + +## MomentumIndicators\.Stoch\(this PriceSeries, int, int, MAType, int, MAType\) Method + +Computes the slow stochastic oscillator\. + +```csharp +public static TechnicalAnalysis.Functions.StochSeries Stoch(this TechnicalAnalysis.Functions.PriceSeries prices, int fastKPeriod=5, int slowKPeriod=3, TechnicalAnalysis.Common.MAType slowKMAType=TechnicalAnalysis.Common.MAType.Sma, int slowDPeriod=3, TechnicalAnalysis.Common.MAType slowDMAType=TechnicalAnalysis.Common.MAType.Sma); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. It must carry high and low prices, because the oscillator measures where +the close sits inside the recent range\. + + + +`fastKPeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The lookback of the raw %K\. Defaults to 5, as in the raw layer\. + + + +`slowKPeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The smoothing period applied to %K\. Defaults to 3, as in the raw layer\. + + + +`slowKMAType` [TechnicalAnalysis\.Common\.MAType](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.matype 'TechnicalAnalysis\.Common\.MAType') + +The moving average used to smooth %K\. Defaults to simple\. + + + +`slowDPeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The period of %D\. Defaults to 3, as in the raw layer\. + + + +`slowDMAType` [TechnicalAnalysis\.Common\.MAType](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.matype 'TechnicalAnalysis\.Common\.MAType') + +The moving average used for %D\. Defaults to simple\. + +#### Returns +[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries') +The %K and %D lines, each independently bar\-aligned\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[fastKPeriod](MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md#TechnicalAnalysis.Functions.MomentumIndicators.Stoch(thisTechnicalAnalysis.Functions.PriceSeries,int,int,TechnicalAnalysis.Common.MAType,int,TechnicalAnalysis.Common.MAType).fastKPeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Stoch\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, TechnicalAnalysis\.Common\.MAType, int, TechnicalAnalysis\.Common\.MAType\)\.fastKPeriod'), [slowKPeriod](MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md#TechnicalAnalysis.Functions.MomentumIndicators.Stoch(thisTechnicalAnalysis.Functions.PriceSeries,int,int,TechnicalAnalysis.Common.MAType,int,TechnicalAnalysis.Common.MAType).slowKPeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Stoch\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, TechnicalAnalysis\.Common\.MAType, int, TechnicalAnalysis\.Common\.MAType\)\.slowKPeriod') or + [slowDPeriod](MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md#TechnicalAnalysis.Functions.MomentumIndicators.Stoch(thisTechnicalAnalysis.Functions.PriceSeries,int,int,TechnicalAnalysis.Common.MAType,int,TechnicalAnalysis.Common.MAType).slowDPeriod 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Stoch\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, TechnicalAnalysis\.Common\.MAType, int, TechnicalAnalysis\.Common\.MAType\)\.slowDPeriod') is outside 2 to 100000\. + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The price series carries no high and low prices\. \ No newline at end of file diff --git a/docs/functions/MomentumIndicators.md b/docs/functions/MomentumIndicators.md new file mode 100644 index 00000000..a5d711b9 --- /dev/null +++ b/docs/functions/MomentumIndicators.md @@ -0,0 +1,30 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## MomentumIndicators Class + +Fluent momentum indicators\. + +```csharp +public static class MomentumIndicators +``` + +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ MomentumIndicators + +### Remarks + +The class name is TA-Lib's own function group and never appears at a call site: these are +extension methods on [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries'). + +No indicator here returns a thresholded verdict such as "overbought". A wrong number looks +wrong; a wrong [System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') looks authoritative and has already destroyed the evidence +that would have shown it was wrong. The conventional levels are also conventions β€” 70 and 30 +come from a 1978 book about daily bars β€” so they belong in your source file, under your review, +not in the library. + +| Methods | | +| :--- | :--- | +| [Adx\(this PriceSeries, int\)](MomentumIndicators.Adx(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Adx\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') | Computes the average directional index β€” the strength of a trend, without its direction\. | +| [Macd\(this PriceSeries, int, int, int\)](MomentumIndicators.Macd(thisPriceSeries,int,int,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Macd\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, int\)') | Computes the moving average convergence divergence of the closing prices\. | +| [Rsi\(this PriceSeries, int\)](MomentumIndicators.Rsi(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Rsi\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') | Computes the relative strength index of the closing prices\. | +| [Stoch\(this PriceSeries, int, int, MAType, int, MAType\)](MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Stoch\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, TechnicalAnalysis\.Common\.MAType, int, TechnicalAnalysis\.Common\.MAType\)') | Computes the slow stochastic oscillator\. | diff --git a/docs/functions/MovingAverageResult.md b/docs/functions/MovingAverageResult.md index fcca7067..9d77caaa 100644 --- a/docs/functions/MovingAverageResult.md +++ b/docs/functions/MovingAverageResult.md @@ -9,7 +9,7 @@ Represents the result of calculating a Moving Average indicator\. public record MovingAverageResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MovingAverageResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MovingAverageResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MovingAverageResult](MovingAverageResult.md 'TechnicalAnalysis\.Functions\.MovingAverageResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MovingAverageVariablePeriodResult.md b/docs/functions/MovingAverageVariablePeriodResult.md index 3308c7ea..bdb38066 100644 --- a/docs/functions/MovingAverageVariablePeriodResult.md +++ b/docs/functions/MovingAverageVariablePeriodResult.md @@ -9,7 +9,7 @@ Represents the result of calculating a Moving Average with Variable Period \(MAV public record MovingAverageVariablePeriodResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MovingAverageVariablePeriodResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MovingAverageVariablePeriodResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MovingAverageVariablePeriodResult](MovingAverageVariablePeriodResult.md 'TechnicalAnalysis\.Functions\.MovingAverageVariablePeriodResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/MultResult.md b/docs/functions/MultResult.md index 232504e5..458f9857 100644 --- a/docs/functions/MultResult.md +++ b/docs/functions/MultResult.md @@ -9,7 +9,7 @@ Represents the result of the vector multiplication operation \(MULT function\)\. public record MultResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 MultResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ MultResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[MultResult](MultResult.md 'TechnicalAnalysis\.Functions\.MultResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/NatrResult.md b/docs/functions/NatrResult.md index ebb13754..4bcafa99 100644 --- a/docs/functions/NatrResult.md +++ b/docs/functions/NatrResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Normalized Average True Range \(NATR\) public record NatrResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 NatrResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ NatrResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[NatrResult](NatrResult.md 'TechnicalAnalysis\.Functions\.NatrResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/ObvResult.md b/docs/functions/ObvResult.md index 92cb960d..5b8d52f0 100644 --- a/docs/functions/ObvResult.md +++ b/docs/functions/ObvResult.md @@ -9,7 +9,7 @@ Represents the result of the On Balance Volume \(OBV\) indicator calculation\. public record ObvResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 ObvResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ ObvResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[ObvResult](ObvResult.md 'TechnicalAnalysis\.Functions\.ObvResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md b/docs/functions/OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md new file mode 100644 index 00000000..fbac16b1 --- /dev/null +++ b/docs/functions/OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md @@ -0,0 +1,53 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[OverlapStudyIndicators](OverlapStudyIndicators.md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators') + +## OverlapStudyIndicators\.BollingerBands\(this PriceSeries, int, double, double, MAType\) Method + +Computes Bollinger Bands over the closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.BollingerBandsSeries BollingerBands(this TechnicalAnalysis.Functions.PriceSeries prices, int timePeriod=5, double nbDevUp=2.0, double nbDevDn=2.0, TechnicalAnalysis.Common.MAType maType=TechnicalAnalysis.Common.MAType.Sma); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. + + + +`timePeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The period of the middle band and of the standard deviation\. \Defaults to 5, which is +TA\-Lib's own default\, even though 20 is the conventional trading choice; pass 20 +explicitly if that is what you want\. The fluent layer never silently redefines a default\. + + + +`nbDevUp` [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double') + +The number of standard deviations for the upper band\. Defaults to 2\. Not validated: any +finite deviation is meaningful\. + + + +`nbDevDn` [System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double') + +The number of standard deviations for the lower band\. Defaults to 2\. Not validated\. + + + +`maType` [TechnicalAnalysis\.Common\.MAType](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.matype 'TechnicalAnalysis\.Common\.MAType') + +The type of moving average used for the middle band\. Defaults to simple\. + +#### Returns +[BollingerBandsSeries](BollingerBandsSeries.md 'TechnicalAnalysis\.Functions\.BollingerBandsSeries') +The three bands, each independently bar\-aligned and sharing the same first bar\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[timePeriod](OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md#TechnicalAnalysis.Functions.OverlapStudyIndicators.BollingerBands(thisTechnicalAnalysis.Functions.PriceSeries,int,double,double,TechnicalAnalysis.Common.MAType).timePeriod 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.BollingerBands\(this TechnicalAnalysis\.Functions\.PriceSeries, int, double, double, TechnicalAnalysis\.Common\.MAType\)\.timePeriod') is outside 2 to 100000\. \ No newline at end of file diff --git a/docs/functions/OverlapStudyIndicators.Ema(thisPriceSeries,int).md b/docs/functions/OverlapStudyIndicators.Ema(thisPriceSeries,int).md new file mode 100644 index 00000000..02d2090b --- /dev/null +++ b/docs/functions/OverlapStudyIndicators.Ema(thisPriceSeries,int).md @@ -0,0 +1,33 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[OverlapStudyIndicators](OverlapStudyIndicators.md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators') + +## OverlapStudyIndicators\.Ema\(this PriceSeries, int\) Method + +Computes the exponential moving average of the closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Ema(this TechnicalAnalysis.Functions.PriceSeries prices, int timePeriod=30); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. + + + +`timePeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The smoothing period\. Defaults to 30, as in the raw layer\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A bar\-aligned series\. If the series holds fewer bars than the period needs, the result is +empty and reports success\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[timePeriod](OverlapStudyIndicators.Ema(thisPriceSeries,int).md#TechnicalAnalysis.Functions.OverlapStudyIndicators.Ema(thisTechnicalAnalysis.Functions.PriceSeries,int).timePeriod 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Ema\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)\.timePeriod') is outside 2 to 100000\. \ No newline at end of file diff --git a/docs/functions/OverlapStudyIndicators.Sma(thisPriceSeries,int).md b/docs/functions/OverlapStudyIndicators.Sma(thisPriceSeries,int).md new file mode 100644 index 00000000..ed6d8c27 --- /dev/null +++ b/docs/functions/OverlapStudyIndicators.Sma(thisPriceSeries,int).md @@ -0,0 +1,33 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[OverlapStudyIndicators](OverlapStudyIndicators.md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators') + +## OverlapStudyIndicators\.Sma\(this PriceSeries, int\) Method + +Computes the simple moving average of the closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Sma(this TechnicalAnalysis.Functions.PriceSeries prices, int timePeriod=30); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. + + + +`timePeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The number of bars to average\. Defaults to 30, as in the raw layer\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A bar\-aligned series whose first bar is `timePeriod - 1`\. If the series holds fewer bars +than the period needs, the result is empty and reports success β€” that is not an error\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[timePeriod](OverlapStudyIndicators.Sma(thisPriceSeries,int).md#TechnicalAnalysis.Functions.OverlapStudyIndicators.Sma(thisTechnicalAnalysis.Functions.PriceSeries,int).timePeriod 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Sma\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)\.timePeriod') is outside 2 to 100000\. \ No newline at end of file diff --git a/docs/functions/OverlapStudyIndicators.md b/docs/functions/OverlapStudyIndicators.md new file mode 100644 index 00000000..504015c1 --- /dev/null +++ b/docs/functions/OverlapStudyIndicators.md @@ -0,0 +1,28 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## OverlapStudyIndicators Class + +Fluent overlap study indicators β€” those plotted on the price scale\. + +```csharp +public static class OverlapStudyIndicators +``` + +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ OverlapStudyIndicators + +### Remarks + +The class name is TA-Lib's own function group and never appears at a call site: these are +extension methods on [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries'), so they appear directly on `prices.` in +IntelliSense. + +Every default here is copied verbatim from the corresponding raw entry point. The fluent layer +never invents a number, so that a fluent call and a raw call with the same arguments cannot +disagree. + +| Methods | | +| :--- | :--- | +| [BollingerBands\(this PriceSeries, int, double, double, MAType\)](OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.BollingerBands\(this TechnicalAnalysis\.Functions\.PriceSeries, int, double, double, TechnicalAnalysis\.Common\.MAType\)') | Computes Bollinger Bands over the closing prices\. | +| [Ema\(this PriceSeries, int\)](OverlapStudyIndicators.Ema(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Ema\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') | Computes the exponential moving average of the closing prices\. | +| [Sma\(this PriceSeries, int\)](OverlapStudyIndicators.Sma(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Sma\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') | Computes the simple moving average of the closing prices\. | diff --git a/docs/functions/PlusDIResult.md b/docs/functions/PlusDIResult.md index defed017..95a77b06 100644 --- a/docs/functions/PlusDIResult.md +++ b/docs/functions/PlusDIResult.md @@ -10,7 +10,7 @@ Represents the result of the Plus Directional Indicator \(\+DI\) calculation\. public record PlusDIResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 PlusDIResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ PlusDIResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[PlusDIResult](PlusDIResult.md 'TechnicalAnalysis\.Functions\.PlusDIResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/PlusDMResult.md b/docs/functions/PlusDMResult.md index 7d873a51..7b501b6b 100644 --- a/docs/functions/PlusDMResult.md +++ b/docs/functions/PlusDMResult.md @@ -10,7 +10,7 @@ Represents the result of the Plus Directional Movement \(\+DM\) indicator calcul public record PlusDMResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 PlusDMResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ PlusDMResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[PlusDMResult](PlusDMResult.md 'TechnicalAnalysis\.Functions\.PlusDMResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/PpoResult.md b/docs/functions/PpoResult.md index b05843ee..3c2cc9c0 100644 --- a/docs/functions/PpoResult.md +++ b/docs/functions/PpoResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Percentage Price Oscillator \(PPO\) ind public record PpoResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 PpoResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ PpoResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[PpoResult](PpoResult.md 'TechnicalAnalysis\.Functions\.PpoResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/PriceSeries.Align.md b/docs/functions/PriceSeries.Align.md new file mode 100644 index 00000000..73b6a0e4 --- /dev/null +++ b/docs/functions/PriceSeries.Align.md @@ -0,0 +1,117 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Align Method + +| Overloads | | +| :--- | :--- | +| [Align\(SingleOutputResult\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)') | Bar\-aligns the result of any single\-output `TAMath` call made over this price series\. | +| [Align<TResult>\(TResult, Func<TResult,double\[\]>\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)') | Bar\-aligns one output of any multi\-output `TAMath` call made over this price series\. | + + + +## PriceSeries\.Align\(SingleOutputResult\) Method + +Bar\-aligns the result of any single\-output `TAMath` call made over this price series\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Align(TechnicalAnalysis.Common.SingleOutputResult result); +``` +#### Parameters + + + +`result` [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') + +The raw result\. It must have been computed over this series with a start index of `0` +and an end index of `BarCount - 1`, otherwise its alignment metadata does not describe +these bars\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +An [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') addressed by BAR index\. + +#### Exceptions + +[System\.ArgumentNullException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception 'System\.ArgumentNullException') +[result](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult).result 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)\.result') is `null`\. + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +The result's alignment metadata is inconsistent with this series β€” its +`BegIdx + NBElement` does not fit inside [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount'), or its +`NBElement` exceeds its own output array\. That is a defect in the indicator rather than +in this call, and it is surfaced rather than clamped because clamping would hand back a +silently shifted series\. The parameter named by the exception is +[result](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult).result 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)\.result')\. + +### Remarks + +This is the escape hatch to the roughly eighty single-output indicators that have no fluent +wrapper yet, and it is the same primitive the shipped wrappers use, so an indicator reached +this way is aligned exactly as carefully as one that ships. The bar count is supplied by this +series, so it can never be mismatched. + +The values are copied.`result.Real` stays the caller's array and may be + post-processed in place afterwards without disturbing the series handed back, which is what + makes [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') unconditionally immutable rather than immutable by + convention. The copy is O(`NBElement`) against an O(n) indicator computation. + +Naming your own extension methods. The fluent indicators are extension methods on + this type declared in this namespace, and the set of them will grow towards the full TA-Lib + surface. A user-authored `public static IndicatorSeries Cci(this PriceSeries, int)` + therefore becomes ambiguous (CS0121) the day the library ships its own `Cci`. Give your + own extensions names the library will never take β€” a prefix such as `MyCci`, or a + receiver type of your own β€” and treat the arrival of new indicators as potentially + source-breaking for code that does otherwise. + + + +## PriceSeries\.Align\\(TResult, Func\\) Method + +Bar\-aligns one output of any multi\-output `TAMath` call made over this price series\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries Align(TResult result, System.Func output) + where TResult : TechnicalAnalysis.Common.IndicatorResult; +``` +#### Type parameters + + + +`TResult` + +The concrete result type\. +#### Parameters + + + +`result` [TResult](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_).TResult 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)\.TResult') + +The raw result\. It must have been computed over this series with a start index of `0` +and an end index of `BarCount - 1`\. + + + +`output` [System\.Func<](https://learn.microsoft.com/en-us/dotnet/api/system.func-2 'System\.Func\`2')[TResult](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_).TResult 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)\.TResult')[,](https://learn.microsoft.com/en-us/dotnet/api/system.func-2 'System\.Func\`2')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[\[\]](https://learn.microsoft.com/en-us/dotnet/api/system.array 'System\.Array')[>](https://learn.microsoft.com/en-us/dotnet/api/system.func-2 'System\.Func\`2') + +Selects the output array to align, from the same result whose metadata is used\. Taking a +selector rather than a separate array is what prevents one result's metadata being paired +with another result's values, which would be a brand new way to misalign a series\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +An [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') addressed by BAR index\. + +#### Exceptions + +[System\.ArgumentNullException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception 'System\.ArgumentNullException') +[result](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_).result 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)\.result') or [output](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_).output 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)\.output') is `null`\. + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +The result's alignment metadata is inconsistent with this series; see +[Align\(SingleOutputResult\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)')\. The parameter named by the exception is +[result](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_).result 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)\.result')\. + +### Remarks +The selected values are copied, so the result's own array remains the caller's to mutate\. +The naming guidance on [Align\(SingleOutputResult\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)') applies here too\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.AsOf(int).md b/docs/functions/PriceSeries.AsOf(int).md new file mode 100644 index 00000000..d0658ecb --- /dev/null +++ b/docs/functions/PriceSeries.AsOf(int).md @@ -0,0 +1,34 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.AsOf\(int\) Method + +Returns the same price series truncated so that it ends at the given BAR index\. + +```csharp +public TechnicalAnalysis.Functions.PriceSeries AsOf(int bar); +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The last BAR index the narrowed series is allowed to know about, with domain +`[0, BarCount)`\. + +#### Returns +[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') +A series with [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') equal to `bar + 1`, sharing the same underlying +data\. Every span truncates accordingly, and bar indices are not rebased\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](PriceSeries.AsOf(int).md#TechnicalAnalysis.Functions.PriceSeries.AsOf(int).bar 'TechnicalAnalysis\.Functions\.PriceSeries\.AsOf\(int\)\.bar') is negative or greater than or equal to [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount')\. + +### Remarks +This is how a backtest hands a strategy the prices without handing it the future: the later +bars are not part of the value at all, so no discipline is required to avoid reading them\. +It is allocation\-free, which is what makes it usable as the default idiom inside a per\-bar +loop\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.BarCount.md b/docs/functions/PriceSeries.BarCount.md new file mode 100644 index 00000000..969ca286 --- /dev/null +++ b/docs/functions/PriceSeries.BarCount.md @@ -0,0 +1,15 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.BarCount Property + +Gets the number of bars in the series\. + +```csharp +public int BarCount { get; } +``` + +#### Property Value +[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') +A count, not an index\. Every BAR index on this type and on the indicator series derived from +it has domain `[0, BarCount)`\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.Close.md b/docs/functions/PriceSeries.Close.md new file mode 100644 index 00000000..e84e7309 --- /dev/null +++ b/docs/functions/PriceSeries.Close.md @@ -0,0 +1,15 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Close Property + +Gets the closing prices\. + +```csharp +public System.ReadOnlySpan Close { get; } +``` + +#### Property Value +[System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') +A read\-only span of exactly [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') elements indexed by BAR index; an empty +span when the series holds no bars\. Every factory requires closes, so this never throws\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.Empty.md b/docs/functions/PriceSeries.Empty.md new file mode 100644 index 00000000..6b422c28 --- /dev/null +++ b/docs/functions/PriceSeries.Empty.md @@ -0,0 +1,28 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Empty Property + +Gets the empty price series\. + +```csharp +public static TechnicalAnalysis.Functions.PriceSeries Empty { get; } +``` + +#### Property Value +[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +A series of zero bars carrying no components at all. Equivalent to +`default(PriceSeries)`, which is what makes it a usable pre-roll state for a field that +is filled in later. + +It carries no high, low or volume, so the indicators that need them throw.[Sma\(this PriceSeries, int\)](OverlapStudyIndicators.Sma(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Sma\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)'), [Ema\(this PriceSeries, int\)](OverlapStudyIndicators.Ema(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.Ema\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)'), + [BollingerBands\(this PriceSeries, int, double, double, MAType\)](OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md 'TechnicalAnalysis\.Functions\.OverlapStudyIndicators\.BollingerBands\(this TechnicalAnalysis\.Functions\.PriceSeries, int, double, double, TechnicalAnalysis\.Common\.MAType\)'), [Rsi\(this PriceSeries, int\)](MomentumIndicators.Rsi(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Rsi\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') + and [Macd\(this PriceSeries, int, int, int\)](MomentumIndicators.Macd(thisPriceSeries,int,int,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Macd\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, int\)') need only closes and return an empty result; + [Atr\(this PriceSeries, int\)](VolatilityIndicators.Atr(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.VolatilityIndicators\.Atr\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)'), [Adx\(this PriceSeries, int\)](MomentumIndicators.Adx(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Adx\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') and + [Stoch\(this PriceSeries, int, int, MAType, int, MAType\)](MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md 'TechnicalAnalysis\.Functions\.MomentumIndicators\.Stoch\(this TechnicalAnalysis\.Functions\.PriceSeries, int, int, TechnicalAnalysis\.Common\.MAType, int, TechnicalAnalysis\.Common\.MAType\)') raise [System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') because + component availability is checked before emptiness, and + [Obv\(this PriceSeries\)](VolumeIndicators.Obv(thisPriceSeries).md 'TechnicalAnalysis\.Functions\.VolumeIndicators\.Obv\(this TechnicalAnalysis\.Functions\.PriceSeries\)') raises it for the missing volumes. A feed that must + answer every indicator while it has no bars yet is + `PriceSeries.FromOhlcv([], [], [], [], [])` β€” the components are then present and + merely empty. \ No newline at end of file diff --git a/docs/functions/PriceSeries.Equals.md b/docs/functions/PriceSeries.Equals.md new file mode 100644 index 00000000..557fabb7 --- /dev/null +++ b/docs/functions/PriceSeries.Equals.md @@ -0,0 +1,58 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Equals Method + +| Overloads | | +| :--- | :--- | +| [Equals\(object\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(object) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(object\)') | Determines whether this price series equals the given object\. | +| [Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)') | Determines whether this price series equals another\. | + + + +## PriceSeries\.Equals\(object\) Method + +Determines whether this price series equals the given object\. + +```csharp +public override bool Equals(object? obj); +``` +#### Parameters + + + +`obj` [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') + +The object to compare with\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when [obj](PriceSeries.md#TechnicalAnalysis.Functions.PriceSeries.Equals(object).obj 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(object\)\.obj') is a [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') equal to this one + under [Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)'); otherwise `false`\. + + + +## PriceSeries\.Equals\(PriceSeries\) Method + +Determines whether this price series equals another\. + +```csharp +public bool Equals(TechnicalAnalysis.Functions.PriceSeries other); +``` +#### Parameters + + + +`other` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The series to compare with\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when both series share every component array \by reference\ and expose the + same number of bars\. + +### Remarks +This does not compare prices\. Two series built from identical inputs are not equal, because +the factories copy and therefore hold different arrays\. Equality exists so that this value +type satisfies CA1815\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.FromClose(ReadOnlySpan_double_).md b/docs/functions/PriceSeries.FromClose(ReadOnlySpan_double_).md new file mode 100644 index 00000000..09121a89 --- /dev/null +++ b/docs/functions/PriceSeries.FromClose(ReadOnlySpan_double_).md @@ -0,0 +1,30 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.FromClose\(ReadOnlySpan\\) Method + +Creates a price series from closing prices alone\. + +```csharp +public static TechnicalAnalysis.Functions.PriceSeries FromClose(System.ReadOnlySpan close); +``` +#### Parameters + + + +`close` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The closing prices, copied into the new series\. May be empty\. + +#### Returns +[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') +A series with [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') equal to the length of [close](PriceSeries.FromClose(ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromClose(System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromClose\(System\.ReadOnlySpan\\)\.close')\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[close](PriceSeries.FromClose(ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromClose(System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromClose\(System\.ReadOnlySpan\\)\.close') holds a value that is not finite\. + +### Remarks +The resulting series carries no open, high, low or volume, and does not fabricate them\. +Indicators that need a bar's range throw rather than silently computing something else\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md b/docs/functions/PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md new file mode 100644 index 00000000..8174a209 --- /dev/null +++ b/docs/functions/PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md @@ -0,0 +1,39 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.FromHlc\(ReadOnlySpan\, ReadOnlySpan\, ReadOnlySpan\\) Method + +Creates a price series from high, low and closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.PriceSeries FromHlc(System.ReadOnlySpan high, System.ReadOnlySpan low, System.ReadOnlySpan close); +``` +#### Parameters + + + +`high` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The high prices\. Must be the same length as [close](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`low` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The low prices\. Must be the same length as [close](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`close` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The closing prices, which define the number of bars\. May be empty\. + +#### Returns +[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') +A series with [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') equal to the length of [close](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[high](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).high 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.high') or [low](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).low 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.low') has a different length from + [close](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close'), or any component holds a value that is not finite\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md b/docs/functions/PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md new file mode 100644 index 00000000..67caef1c --- /dev/null +++ b/docs/functions/PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md @@ -0,0 +1,46 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.FromOhlc\(ReadOnlySpan\, ReadOnlySpan\, ReadOnlySpan\, ReadOnlySpan\\) Method + +Creates a price series from open, high, low and closing prices\. + +```csharp +public static TechnicalAnalysis.Functions.PriceSeries FromOhlc(System.ReadOnlySpan open, System.ReadOnlySpan high, System.ReadOnlySpan low, System.ReadOnlySpan close); +``` +#### Parameters + + + +`open` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The opening prices\. Must be the same length as [close](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`high` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The high prices\. Must be the same length as [close](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`low` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The low prices\. Must be the same length as [close](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`close` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The closing prices, which define the number of bars\. May be empty\. + +#### Returns +[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') +A series with [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') equal to the length of [close](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[open](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).open 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.open'), [high](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).high 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.high') or [low](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).low 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.low') has a different + length from [close](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close'), or any component holds a value that is not finite\. + Components are checked in the order open, high, low\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md b/docs/functions/PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md new file mode 100644 index 00000000..ea96ad0a --- /dev/null +++ b/docs/functions/PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md @@ -0,0 +1,53 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.FromOhlcv\(ReadOnlySpan\, ReadOnlySpan\, ReadOnlySpan\, ReadOnlySpan\, ReadOnlySpan\\) Method + +Creates a price series from open, high, low and closing prices together with volumes\. + +```csharp +public static TechnicalAnalysis.Functions.PriceSeries FromOhlcv(System.ReadOnlySpan open, System.ReadOnlySpan high, System.ReadOnlySpan low, System.ReadOnlySpan close, System.ReadOnlySpan volume); +``` +#### Parameters + + + +`open` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The opening prices\. Must be the same length as [close](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`high` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The high prices\. Must be the same length as [close](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`low` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The low prices\. Must be the same length as [close](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + + + +`close` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The closing prices, which define the number of bars\. May be empty\. + + + +`volume` [System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') + +The volumes\. Must be the same length as [close](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + +#### Returns +[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') +A series with [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') equal to the length of [close](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close')\. + +#### Exceptions + +[System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') +[open](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).open 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.open'), [high](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).high 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.high'), [low](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).low 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.low') or + [volume](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).volume 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.volume') has a different length from [close](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md#TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_,System.ReadOnlySpan_double_).close 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)\.close'), or any + component holds a value that is not finite\. Components are checked in the order open, high, + low, volume\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.GetHashCode().md b/docs/functions/PriceSeries.GetHashCode().md new file mode 100644 index 00000000..27ad0dea --- /dev/null +++ b/docs/functions/PriceSeries.GetHashCode().md @@ -0,0 +1,14 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.GetHashCode\(\) Method + +Returns a hash code consistent with [Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)')\. + +```csharp +public override int GetHashCode(); +``` + +#### Returns +[System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') +A hash code derived from the identity of the component arrays and the bar count\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.HasHighLow.md b/docs/functions/PriceSeries.HasHighLow.md new file mode 100644 index 00000000..70a14aef --- /dev/null +++ b/docs/functions/PriceSeries.HasHighLow.md @@ -0,0 +1,16 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.HasHighLow Property + +Gets a value indicating whether the series carries high and low prices\. + +```csharp +public bool HasHighLow { get; } +``` + +#### Property Value +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the series was built by [FromHlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)'), [FromOhlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)') or + [FromOhlcv\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)')\. Indicators that need a bar's range, such as ATR and the stochastic + oscillator, require this\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.HasOpen.md b/docs/functions/PriceSeries.HasOpen.md new file mode 100644 index 00000000..87353c0a --- /dev/null +++ b/docs/functions/PriceSeries.HasOpen.md @@ -0,0 +1,14 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.HasOpen Property + +Gets a value indicating whether the series carries opening prices\. + +```csharp +public bool HasOpen { get; } +``` + +#### Property Value +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the series was built by [FromOhlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)') or [FromOhlcv\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)')\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.HasVolume.md b/docs/functions/PriceSeries.HasVolume.md new file mode 100644 index 00000000..12bae98a --- /dev/null +++ b/docs/functions/PriceSeries.HasVolume.md @@ -0,0 +1,14 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.HasVolume Property + +Gets a value indicating whether the series carries volumes\. + +```csharp +public bool HasVolume { get; } +``` + +#### Property Value +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the series was built by [FromOhlcv\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)')\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.High.md b/docs/functions/PriceSeries.High.md new file mode 100644 index 00000000..c0f37745 --- /dev/null +++ b/docs/functions/PriceSeries.High.md @@ -0,0 +1,19 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.High Property + +Gets the high prices\. + +```csharp +public System.ReadOnlySpan High { get; } +``` + +#### Property Value +[System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') +A read\-only span of exactly [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') elements indexed by BAR index\. + +#### Exceptions + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The series carries no high prices; see [HasHighLow](PriceSeries.HasHighLow.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasHighLow')\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.IsEmpty.md b/docs/functions/PriceSeries.IsEmpty.md new file mode 100644 index 00000000..484a63af --- /dev/null +++ b/docs/functions/PriceSeries.IsEmpty.md @@ -0,0 +1,14 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.IsEmpty Property + +Gets a value indicating whether the series holds no bars\. + +```csharp +public bool IsEmpty { get; } +``` + +#### Property Value +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') is zero\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.Low.md b/docs/functions/PriceSeries.Low.md new file mode 100644 index 00000000..98604aba --- /dev/null +++ b/docs/functions/PriceSeries.Low.md @@ -0,0 +1,19 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Low Property + +Gets the low prices\. + +```csharp +public System.ReadOnlySpan Low { get; } +``` + +#### Property Value +[System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') +A read\-only span of exactly [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') elements indexed by BAR index\. + +#### Exceptions + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The series carries no low prices; see [HasHighLow](PriceSeries.HasHighLow.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasHighLow')\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.Open.md b/docs/functions/PriceSeries.Open.md new file mode 100644 index 00000000..b2e02cf3 --- /dev/null +++ b/docs/functions/PriceSeries.Open.md @@ -0,0 +1,19 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Open Property + +Gets the opening prices\. + +```csharp +public System.ReadOnlySpan Open { get; } +``` + +#### Property Value +[System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') +A read\-only span of exactly [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') elements indexed by BAR index\. + +#### Exceptions + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The series carries no opening prices; see [HasOpen](PriceSeries.HasOpen.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasOpen')\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.Volume.md b/docs/functions/PriceSeries.Volume.md new file mode 100644 index 00000000..c2c93e78 --- /dev/null +++ b/docs/functions/PriceSeries.Volume.md @@ -0,0 +1,19 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.Volume Property + +Gets the volumes\. + +```csharp +public System.ReadOnlySpan Volume { get; } +``` + +#### Property Value +[System\.ReadOnlySpan<](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1')[System\.Double](https://learn.microsoft.com/en-us/dotnet/api/system.double 'System\.Double')[>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1') +A read\-only span of exactly [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') elements indexed by BAR index\. + +#### Exceptions + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The series carries no volumes; see [HasVolume](PriceSeries.HasVolume.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasVolume')\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.md b/docs/functions/PriceSeries.md new file mode 100644 index 00000000..4aee0f1a --- /dev/null +++ b/docs/functions/PriceSeries.md @@ -0,0 +1,88 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## PriceSeries Struct + +An immutable price series β€” the entry point to the fluent indicator API\. + +```csharp +public readonly struct PriceSeries : System.IEquatable +``` + +Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') + +### Example + +```csharp +PriceSeries prices = PriceSeries.FromHlc(highs, lows, closes); +double? rsi = prices.Rsi(14).Latest; +double? atr = prices.Atr(14).Latest; +``` + +### Remarks + +Build one with the factory that matches the data you actually have, then call an indicator on +it. Every position on this type is a BAR index with domain `[0, BarCount)`. + +The factories copy. Immutability is unconditional rather than dependent on caller + discipline, so a [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') and every [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') derived from + it are safe for unrestricted concurrent use with no caveat. Because the factories take + [System\.ReadOnlySpan<>](https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1 'System\.ReadOnlySpan\`1'), a caller with an oversized scratch buffer pays only for the valid + region: `PriceSeries.FromHlc(high.AsSpan(0, count), low.AsSpan(0, count), close.AsSpan(0, count))`. + +The factories reject non-finite prices. A [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') close β€” a gapped tick, + a provider sentinel, a bad CSV parse β€” is refused at the boundary with an + [System\.ArgumentException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception 'System\.ArgumentException') naming the component and the first offending bar. It has to be: + the running sums inside TA-Lib's simple moving average, and the recursions inside the + exponential moving average, the average true range and the relative strength index, all + propagate a single [System\.Double\.NaN](https://learn.microsoft.com/en-us/dotnet/api/system.double.nan 'System\.Double\.NaN') to every later bar. One bad tick would otherwise + poison the whole series while every status flag still read success, and + `if (sma[bar] is { } value)` would keep succeeding with a value that compares + `false` against every threshold. The factories already walk the data to copy it, so the + check costs nothing. + +There is no startIdx or endIdx anywhere on this surface. Indicators always + analyse the full series, so the first bar of a result is always the indicator's true lookback, + and a caller can never trigger the unguarded end-index buffer overrun that a raw + `TAMath` call permits. Windowing is expressed by [AsOf\(int\)](PriceSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.PriceSeries\.AsOf\(int\)'), which keeps bar + indices absolute instead of rebasing them. + +Missing components are never fabricated. A close-only series does not pretend that open, + high and low equal the close; asking for them throws. Otherwise an ATR would silently compute + the absolute change in close and call it a true range. + +There is no caching. Calling `prices.Sma(20)` twice computes twice. Because this is + a value type with no lazy state, caching is one field at the composition root when it is wanted, + and there is no shared mutable state and no locking when it is not. + +| Properties | | +| :--- | :--- | +| [BarCount](PriceSeries.BarCount.md 'TechnicalAnalysis\.Functions\.PriceSeries\.BarCount') | Gets the number of bars in the series\. | +| [Close](PriceSeries.Close.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Close') | Gets the closing prices\. | +| [Empty](PriceSeries.Empty.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Empty') | Gets the empty price series\. | +| [HasHighLow](PriceSeries.HasHighLow.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasHighLow') | Gets a value indicating whether the series carries high and low prices\. | +| [HasOpen](PriceSeries.HasOpen.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasOpen') | Gets a value indicating whether the series carries opening prices\. | +| [HasVolume](PriceSeries.HasVolume.md 'TechnicalAnalysis\.Functions\.PriceSeries\.HasVolume') | Gets a value indicating whether the series carries volumes\. | +| [High](PriceSeries.High.md 'TechnicalAnalysis\.Functions\.PriceSeries\.High') | Gets the high prices\. | +| [IsEmpty](PriceSeries.IsEmpty.md 'TechnicalAnalysis\.Functions\.PriceSeries\.IsEmpty') | Gets a value indicating whether the series holds no bars\. | +| [Low](PriceSeries.Low.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Low') | Gets the low prices\. | +| [Open](PriceSeries.Open.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Open') | Gets the opening prices\. | +| [Volume](PriceSeries.Volume.md 'TechnicalAnalysis\.Functions\.PriceSeries\.Volume') | Gets the volumes\. | + +| Methods | | +| :--- | :--- | +| [Align\(SingleOutputResult\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\(TechnicalAnalysis\.Common\.SingleOutputResult\)') | Bar\-aligns the result of any single\-output `TAMath` call made over this price series\. | +| [Align<TResult>\(TResult, Func<TResult,double\[\]>\)](PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_) 'TechnicalAnalysis\.Functions\.PriceSeries\.Align\\(TResult, System\.Func\\)') | Bar\-aligns one output of any multi\-output `TAMath` call made over this price series\. | +| [AsOf\(int\)](PriceSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.PriceSeries\.AsOf\(int\)') | Returns the same price series truncated so that it ends at the given BAR index\. | +| [Equals\(object\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(object) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(object\)') | Determines whether this price series equals the given object\. | +| [Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)') | Determines whether this price series equals another\. | +| [FromClose\(ReadOnlySpan<double>\)](PriceSeries.FromClose(ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromClose\(System\.ReadOnlySpan\\)') | Creates a price series from closing prices alone\. | +| [FromHlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromHlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)') | Creates a price series from high, low and closing prices\. | +| [FromOhlc\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlc\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)') | Creates a price series from open, high, low and closing prices\. | +| [FromOhlcv\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)') | Creates a price series from open, high, low and closing prices together with volumes\. | +| [GetHashCode\(\)](PriceSeries.GetHashCode().md 'TechnicalAnalysis\.Functions\.PriceSeries\.GetHashCode\(\)') | Returns a hash code consistent with [Equals\(PriceSeries\)](PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries) 'TechnicalAnalysis\.Functions\.PriceSeries\.Equals\(TechnicalAnalysis\.Functions\.PriceSeries\)')\. | + +| Operators | | +| :--- | :--- | +| [operator ==\(PriceSeries, PriceSeries\)](PriceSeries.operator(PriceSeries,PriceSeries).md 'TechnicalAnalysis\.Functions\.PriceSeries\.op\_Equality\(TechnicalAnalysis\.Functions\.PriceSeries, TechnicalAnalysis\.Functions\.PriceSeries\)') | Determines whether two price series are equal\. | +| [operator \!=\(PriceSeries, PriceSeries\)](PriceSeries.operator!(PriceSeries,PriceSeries).md 'TechnicalAnalysis\.Functions\.PriceSeries\.op\_Inequality\(TechnicalAnalysis\.Functions\.PriceSeries, TechnicalAnalysis\.Functions\.PriceSeries\)') | Determines whether two price series are not equal\. | diff --git a/docs/functions/PriceSeries.operator!(PriceSeries,PriceSeries).md b/docs/functions/PriceSeries.operator!(PriceSeries,PriceSeries).md new file mode 100644 index 00000000..2a19b6b4 --- /dev/null +++ b/docs/functions/PriceSeries.operator!(PriceSeries,PriceSeries).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.operator \!=\(PriceSeries, PriceSeries\) Operator + +Determines whether two price series are not equal\. + +```csharp +public static bool operator !=(TechnicalAnalysis.Functions.PriceSeries left, TechnicalAnalysis.Functions.PriceSeries right); +``` +#### Parameters + + + +`left` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The first series\. + + + +`right` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The second series\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the two series are not equal; otherwise `false`\. \ No newline at end of file diff --git a/docs/functions/PriceSeries.operator(PriceSeries,PriceSeries).md b/docs/functions/PriceSeries.operator(PriceSeries,PriceSeries).md new file mode 100644 index 00000000..568804f4 --- /dev/null +++ b/docs/functions/PriceSeries.operator(PriceSeries,PriceSeries).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +## PriceSeries\.operator ==\(PriceSeries, PriceSeries\) Operator + +Determines whether two price series are equal\. + +```csharp +public static bool operator ==(TechnicalAnalysis.Functions.PriceSeries left, TechnicalAnalysis.Functions.PriceSeries right); +``` +#### Parameters + + + +`left` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The first series\. + + + +`right` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The second series\. + +#### Returns +[System\.Boolean](https://learn.microsoft.com/en-us/dotnet/api/system.boolean 'System\.Boolean') +`true` when the two series are equal; otherwise `false`\. \ No newline at end of file diff --git a/docs/functions/RocPResult.md b/docs/functions/RocPResult.md index 4dfa2578..4b246426 100644 --- a/docs/functions/RocPResult.md +++ b/docs/functions/RocPResult.md @@ -10,7 +10,7 @@ This momentum indicator measures the rate of change in price expressed as a deci public record RocPResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 RocPResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ RocPResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[RocPResult](RocPResult.md 'TechnicalAnalysis\.Functions\.RocPResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/RocR100Result.md b/docs/functions/RocR100Result.md index 45a56304..1e9ac80e 100644 --- a/docs/functions/RocR100Result.md +++ b/docs/functions/RocR100Result.md @@ -9,7 +9,7 @@ Represents the result of calculating the Rate of Change Ratio 100 scale \(ROCR10 public record RocR100Result : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 RocR100Result +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ RocR100Result Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[RocR100Result](RocR100Result.md 'TechnicalAnalysis\.Functions\.RocR100Result')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/RocRResult.md b/docs/functions/RocRResult.md index 1a8289f2..fd2c9562 100644 --- a/docs/functions/RocRResult.md +++ b/docs/functions/RocRResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Rate of Change Ratio \(ROCR\) indicator public record RocRResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 RocRResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ RocRResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[RocRResult](RocRResult.md 'TechnicalAnalysis\.Functions\.RocRResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/RocResult.md b/docs/functions/RocResult.md index 1b8b59da..3c9eb213 100644 --- a/docs/functions/RocResult.md +++ b/docs/functions/RocResult.md @@ -10,7 +10,7 @@ This momentum indicator measures the percentage change in price between the curr public record RocResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 RocResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ RocResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[RocResult](RocResult.md 'TechnicalAnalysis\.Functions\.RocResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/RsiResult.md b/docs/functions/RsiResult.md index c0f8c6db..f5bc5a6a 100644 --- a/docs/functions/RsiResult.md +++ b/docs/functions/RsiResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Relative Strength Index \(RSI\) indicat public record RsiResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 RsiResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ RsiResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[RsiResult](RsiResult.md 'TechnicalAnalysis\.Functions\.RsiResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SarExtResult.md b/docs/functions/SarExtResult.md index 2052b6b4..b681953f 100644 --- a/docs/functions/SarExtResult.md +++ b/docs/functions/SarExtResult.md @@ -11,7 +11,7 @@ for acceleration factors and other parameters, providing more flexibility in tre public record SarExtResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SarExtResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SarExtResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SarExtResult](SarExtResult.md 'TechnicalAnalysis\.Functions\.SarExtResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SarResult.md b/docs/functions/SarResult.md index 7731550e..afaee83c 100644 --- a/docs/functions/SarResult.md +++ b/docs/functions/SarResult.md @@ -11,7 +11,7 @@ identify potential reversal points and manage risk in trending markets\. public record SarResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SarResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SarResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SarResult](SarResult.md 'TechnicalAnalysis\.Functions\.SarResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SinResult.md b/docs/functions/SinResult.md index 5231189b..7d9cd5b0 100644 --- a/docs/functions/SinResult.md +++ b/docs/functions/SinResult.md @@ -9,7 +9,7 @@ Represents the result of the vector sine operation \(SIN function\)\. public record SinResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SinResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SinResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SinResult](SinResult.md 'TechnicalAnalysis\.Functions\.SinResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SinhResult.md b/docs/functions/SinhResult.md index 418252e1..343a9689 100644 --- a/docs/functions/SinhResult.md +++ b/docs/functions/SinhResult.md @@ -9,7 +9,7 @@ Represents the result of the vector hyperbolic sine operation \(SINH function\)\ public record SinhResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SinhResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SinhResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SinhResult](SinhResult.md 'TechnicalAnalysis\.Functions\.SinhResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SmaResult.md b/docs/functions/SmaResult.md index 60fdc880..729ddb7b 100644 --- a/docs/functions/SmaResult.md +++ b/docs/functions/SmaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Simple Moving Average \(SMA\) indicator public record SmaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SmaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SmaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SmaResult](SmaResult.md 'TechnicalAnalysis\.Functions\.SmaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SqrtResult.md b/docs/functions/SqrtResult.md index 653562ac..23117b30 100644 --- a/docs/functions/SqrtResult.md +++ b/docs/functions/SqrtResult.md @@ -9,7 +9,7 @@ Represents the result of the vector square root operation \(SQRT function\)\. public record SqrtResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SqrtResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SqrtResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SqrtResult](SqrtResult.md 'TechnicalAnalysis\.Functions\.SqrtResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/StdDevResult.md b/docs/functions/StdDevResult.md index 369ee139..832303b5 100644 --- a/docs/functions/StdDevResult.md +++ b/docs/functions/StdDevResult.md @@ -10,7 +10,7 @@ Standard Deviation is a statistical measure of volatility that shows how much va public record StdDevResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 StdDevResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ StdDevResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[StdDevResult](StdDevResult.md 'TechnicalAnalysis\.Functions\.StdDevResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/StochFResult.md b/docs/functions/StochFResult.md index 3324721a..ad502910 100644 --- a/docs/functions/StochFResult.md +++ b/docs/functions/StochFResult.md @@ -10,7 +10,7 @@ The Fast Stochastic is a momentum indicator that provides a more responsive vers public record StochFResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 StochFResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ StochFResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[StochFResult](StochFResult.md 'TechnicalAnalysis\.Functions\.StochFResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/StochResult.md b/docs/functions/StochResult.md index 071d1e44..1a6dfde2 100644 --- a/docs/functions/StochResult.md +++ b/docs/functions/StochResult.md @@ -10,7 +10,7 @@ The Stochastic Oscillator is a momentum indicator that shows the location of the public record StochResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 StochResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ StochResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[StochResult](StochResult.md 'TechnicalAnalysis\.Functions\.StochResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/StochRsiResult.md b/docs/functions/StochRsiResult.md index f5a79a7a..9cf449a9 100644 --- a/docs/functions/StochRsiResult.md +++ b/docs/functions/StochRsiResult.md @@ -11,7 +11,7 @@ momentum oscillator that combines the benefits of both indicators\. public record StochRsiResult : TechnicalAnalysis.Common.DualOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') 🡒 StochRsiResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.DualOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.dualoutputresult 'TechnicalAnalysis\.Common\.DualOutputResult') β†’ StochRsiResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[StochRsiResult](StochRsiResult.md 'TechnicalAnalysis\.Functions\.StochRsiResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/StochSeries.AsOf(int).md b/docs/functions/StochSeries.AsOf(int).md new file mode 100644 index 00000000..f3a6b4d2 --- /dev/null +++ b/docs/functions/StochSeries.AsOf(int).md @@ -0,0 +1,27 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries') + +## StochSeries\.AsOf\(int\) Method + +Narrows both lines so that they end at the given BAR index\. + +```csharp +public TechnicalAnalysis.Functions.StochSeries AsOf(int bar); +``` +#### Parameters + + + +`bar` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The last BAR index the narrowed lines are allowed to know about, with domain +`[0, BarCount)` of the lines\. + +#### Returns +[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries') +A result whose two lines have each been narrowed by [AsOf\(int\)](IndicatorSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.IndicatorSeries\.AsOf\(int\)')\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[bar](StochSeries.AsOf(int).md#TechnicalAnalysis.Functions.StochSeries.AsOf(int).bar 'TechnicalAnalysis\.Functions\.StochSeries\.AsOf\(int\)\.bar') is outside the bars the lines cover\. \ No newline at end of file diff --git a/docs/functions/StochSeries.SlowD.md b/docs/functions/StochSeries.SlowD.md new file mode 100644 index 00000000..384747e6 --- /dev/null +++ b/docs/functions/StochSeries.SlowD.md @@ -0,0 +1,14 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries') + +## StochSeries\.SlowD Property + +The slow %D line: a moving average of SlowK, acting as its signal line\. +Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries SlowD { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/StochSeries.SlowK.md b/docs/functions/StochSeries.SlowK.md new file mode 100644 index 00000000..93ca445d --- /dev/null +++ b/docs/functions/StochSeries.SlowK.md @@ -0,0 +1,14 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries') + +## StochSeries\.SlowK Property + +The slow %K line: the smoothed position of the close within the recent high\-low range, +expressed from 0 to 100\. Bar\-aligned\. + +```csharp +public TechnicalAnalysis.Functions.IndicatorSeries SlowK { get; init; } +``` + +#### Property Value +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') \ No newline at end of file diff --git a/docs/functions/StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md b/docs/functions/StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md new file mode 100644 index 00000000..d1bd5d28 --- /dev/null +++ b/docs/functions/StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md @@ -0,0 +1,29 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries') + +## StochSeries\(IndicatorSeries, IndicatorSeries\) Constructor + +The two bar\-aligned outputs of a stochastic oscillator calculation\. + +```csharp +public StochSeries(TechnicalAnalysis.Functions.IndicatorSeries SlowK, TechnicalAnalysis.Functions.IndicatorSeries SlowD); +``` +#### Parameters + + + +`SlowK` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The slow %K line: the smoothed position of the close within the recent high\-low range, +expressed from 0 to 100\. Bar\-aligned\. + + + +`SlowD` [IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') + +The slow %D line: a moving average of [SlowK](StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md#TechnicalAnalysis.Functions.StochSeries.StochSeries(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries).SlowK 'TechnicalAnalysis\.Functions\.StochSeries\.StochSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)\.SlowK'), acting as its signal line\. +Bar\-aligned\. + +### Remarks +The component names match [StochResult](StochResult.md 'TechnicalAnalysis\.Functions\.StochResult'), so moving between the raw and fluent layers +costs nothing\. A %K/%D crossing is `stoch.SlowK.CrossedAbove(stoch.SlowD, bar)`\. \ No newline at end of file diff --git a/docs/functions/StochSeries.md b/docs/functions/StochSeries.md new file mode 100644 index 00000000..85cc3718 --- /dev/null +++ b/docs/functions/StochSeries.md @@ -0,0 +1,29 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## StochSeries Struct + +The two bar\-aligned outputs of a stochastic oscillator calculation\. + +```csharp +public readonly record struct StochSeries : System.IEquatable +``` + +Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[StochSeries](StochSeries.md 'TechnicalAnalysis\.Functions\.StochSeries')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') + +### Remarks +The component names match [StochResult](StochResult.md 'TechnicalAnalysis\.Functions\.StochResult'), so moving between the raw and fluent layers +costs nothing\. A %K/%D crossing is `stoch.SlowK.CrossedAbove(stoch.SlowD, bar)`\. + +| Constructors | | +| :--- | :--- | +| [StochSeries\(IndicatorSeries, IndicatorSeries\)](StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md 'TechnicalAnalysis\.Functions\.StochSeries\.StochSeries\(TechnicalAnalysis\.Functions\.IndicatorSeries, TechnicalAnalysis\.Functions\.IndicatorSeries\)') | The two bar\-aligned outputs of a stochastic oscillator calculation\. | + +| Properties | | +| :--- | :--- | +| [SlowD](StochSeries.SlowD.md 'TechnicalAnalysis\.Functions\.StochSeries\.SlowD') | The slow %D line: a moving average of SlowK, acting as its signal line\. Bar\-aligned\. | +| [SlowK](StochSeries.SlowK.md 'TechnicalAnalysis\.Functions\.StochSeries\.SlowK') | The slow %K line: the smoothed position of the close within the recent high\-low range, expressed from 0 to 100\. Bar\-aligned\. | + +| Methods | | +| :--- | :--- | +| [AsOf\(int\)](StochSeries.AsOf(int).md 'TechnicalAnalysis\.Functions\.StochSeries\.AsOf\(int\)') | Narrows both lines so that they end at the given BAR index\. | diff --git a/docs/functions/SubResult.md b/docs/functions/SubResult.md index fc0eb4a4..6e1f489d 100644 --- a/docs/functions/SubResult.md +++ b/docs/functions/SubResult.md @@ -9,7 +9,7 @@ Represents the result of the vector subtraction operation \(SUB function\)\. public record SubResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SubResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SubResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SubResult](SubResult.md 'TechnicalAnalysis\.Functions\.SubResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/SumResult.md b/docs/functions/SumResult.md index 3884ff17..4af31efd 100644 --- a/docs/functions/SumResult.md +++ b/docs/functions/SumResult.md @@ -10,7 +10,7 @@ The SUM function calculates the total of all values within a rolling window\. public record SumResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 SumResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ SumResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[SumResult](SumResult.md 'TechnicalAnalysis\.Functions\.SumResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/T3Result.md b/docs/functions/T3Result.md index 383f76a6..daf3b7f8 100644 --- a/docs/functions/T3Result.md +++ b/docs/functions/T3Result.md @@ -9,7 +9,7 @@ Represents the result of calculating the T3 Moving Average indicator\. public record T3Result : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 T3Result +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ T3Result Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[T3Result](T3Result.md 'TechnicalAnalysis\.Functions\.T3Result')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TAFunc.md b/docs/functions/TAFunc.md index e8b7ec6a..7794e5d3 100644 --- a/docs/functions/TAFunc.md +++ b/docs/functions/TAFunc.md @@ -9,7 +9,7 @@ Provides technical analysis functions for financial market analysis\. public static class TAFunc ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 TAFunc +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ TAFunc ### Remarks This class contains a comprehensive set of technical indicators and mathematical functions diff --git a/docs/functions/TAMath.md b/docs/functions/TAMath.md index cbaa2a63..4a86ef61 100644 --- a/docs/functions/TAMath.md +++ b/docs/functions/TAMath.md @@ -9,7 +9,7 @@ Provides mathematical wrapper functions for technical analysis calculations\. public static class TAMath ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 TAMath +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ TAMath ### Remarks This partial class provides convenient overloads for mathematical functions used in technical analysis\. diff --git a/docs/functions/TanResult.md b/docs/functions/TanResult.md index 251ab3c2..59d1d856 100644 --- a/docs/functions/TanResult.md +++ b/docs/functions/TanResult.md @@ -9,7 +9,7 @@ Represents the result of the vector tangent operation \(TAN function\)\. public record TanResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TanResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TanResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TanResult](TanResult.md 'TechnicalAnalysis\.Functions\.TanResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TanhResult.md b/docs/functions/TanhResult.md index c5004092..c2abedc4 100644 --- a/docs/functions/TanhResult.md +++ b/docs/functions/TanhResult.md @@ -9,7 +9,7 @@ Represents the result of the vector hyperbolic tangent operation \(TANH function public record TanhResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TanhResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TanhResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TanhResult](TanhResult.md 'TechnicalAnalysis\.Functions\.TanhResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TemaResult.md b/docs/functions/TemaResult.md index c1fc46bb..cbbd82a9 100644 --- a/docs/functions/TemaResult.md +++ b/docs/functions/TemaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Triple Exponential Moving Average \(TEM public record TemaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TemaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TemaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TemaResult](TemaResult.md 'TechnicalAnalysis\.Functions\.TemaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TrimaResult.md b/docs/functions/TrimaResult.md index cf2b333d..ea39a0d1 100644 --- a/docs/functions/TrimaResult.md +++ b/docs/functions/TrimaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Triangular Moving Average \(TRIMA\) ind public record TrimaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TrimaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TrimaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TrimaResult](TrimaResult.md 'TechnicalAnalysis\.Functions\.TrimaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TrixResult.md b/docs/functions/TrixResult.md index 3026a1ad..dedf8f61 100644 --- a/docs/functions/TrixResult.md +++ b/docs/functions/TrixResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the TRIX indicator\. public record TrixResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TrixResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TrixResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TrixResult](TrixResult.md 'TechnicalAnalysis\.Functions\.TrixResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TrueRangeResult.md b/docs/functions/TrueRangeResult.md index 3e53d623..f5933467 100644 --- a/docs/functions/TrueRangeResult.md +++ b/docs/functions/TrueRangeResult.md @@ -10,7 +10,7 @@ True Range is a volatility measure that captures the greatest of: current high m public record TrueRangeResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TrueRangeResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TrueRangeResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TrueRangeResult](TrueRangeResult.md 'TechnicalAnalysis\.Functions\.TrueRangeResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TsfResult.md b/docs/functions/TsfResult.md index 53f90c38..ca502a55 100644 --- a/docs/functions/TsfResult.md +++ b/docs/functions/TsfResult.md @@ -11,7 +11,7 @@ forecast of where prices might be based on the current trend\. public record TsfResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TsfResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TsfResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TsfResult](TsfResult.md 'TechnicalAnalysis\.Functions\.TsfResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/TypPriceResult.md b/docs/functions/TypPriceResult.md index 89fefbd2..b67b4958 100644 --- a/docs/functions/TypPriceResult.md +++ b/docs/functions/TypPriceResult.md @@ -9,7 +9,7 @@ Represents the result of the Typical Price calculation\. public record TypPriceResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 TypPriceResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ TypPriceResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[TypPriceResult](TypPriceResult.md 'TechnicalAnalysis\.Functions\.TypPriceResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/UltOscResult.md b/docs/functions/UltOscResult.md index f86e78ec..cc1ccd34 100644 --- a/docs/functions/UltOscResult.md +++ b/docs/functions/UltOscResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Ultimate Oscillator \(UltOsc\) indicato public record UltOscResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 UltOscResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ UltOscResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[UltOscResult](UltOscResult.md 'TechnicalAnalysis\.Functions\.UltOscResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/VarianceResult.md b/docs/functions/VarianceResult.md index 0d1f15f1..83665d86 100644 --- a/docs/functions/VarianceResult.md +++ b/docs/functions/VarianceResult.md @@ -10,7 +10,7 @@ Variance is a statistical measure of volatility that represents the squared devi public record VarianceResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 VarianceResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ VarianceResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[VarianceResult](VarianceResult.md 'TechnicalAnalysis\.Functions\.VarianceResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/VolatilityIndicators.Atr(thisPriceSeries,int).md b/docs/functions/VolatilityIndicators.Atr(thisPriceSeries,int).md new file mode 100644 index 00000000..8da67670 --- /dev/null +++ b/docs/functions/VolatilityIndicators.Atr(thisPriceSeries,int).md @@ -0,0 +1,38 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[VolatilityIndicators](VolatilityIndicators.md 'TechnicalAnalysis\.Functions\.VolatilityIndicators') + +## VolatilityIndicators\.Atr\(this PriceSeries, int\) Method + +Computes the average true range\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Atr(this TechnicalAnalysis.Functions.PriceSeries prices, int timePeriod=14); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. It must carry high and low prices: a true range is a property of a bar's +range, and the absolute change in close is not a substitute for it\. + + + +`timePeriod` [System\.Int32](https://learn.microsoft.com/en-us/dotnet/api/system.int32 'System\.Int32') + +The smoothing period\. Defaults to 14, as in the raw layer\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A bar\-aligned series whose first bar is [timePeriod](VolatilityIndicators.Atr(thisPriceSeries,int).md#TechnicalAnalysis.Functions.VolatilityIndicators.Atr(thisTechnicalAnalysis.Functions.PriceSeries,int).timePeriod 'TechnicalAnalysis\.Functions\.VolatilityIndicators\.Atr\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)\.timePeriod')\. If the series holds +fewer bars than the period needs, the result is empty and reports success\. + +#### Exceptions + +[System\.ArgumentOutOfRangeException](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception 'System\.ArgumentOutOfRangeException') +[timePeriod](VolatilityIndicators.Atr(thisPriceSeries,int).md#TechnicalAnalysis.Functions.VolatilityIndicators.Atr(thisTechnicalAnalysis.Functions.PriceSeries,int).timePeriod 'TechnicalAnalysis\.Functions\.VolatilityIndicators\.Atr\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)\.timePeriod') is outside 2 to 100000\. This is deliberately stricter than the + raw entry point, which accepts 1, in exchange for one period rule that holds everywhere\. + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The price series carries no high and low prices\. \ No newline at end of file diff --git a/docs/functions/VolatilityIndicators.md b/docs/functions/VolatilityIndicators.md new file mode 100644 index 00000000..b0398509 --- /dev/null +++ b/docs/functions/VolatilityIndicators.md @@ -0,0 +1,20 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## VolatilityIndicators Class + +Fluent volatility indicators\. + +```csharp +public static class VolatilityIndicators +``` + +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ VolatilityIndicators + +### Remarks +The class name is TA\-Lib's own function group and never appears at a call site: these are +extension methods on [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries')\. + +| Methods | | +| :--- | :--- | +| [Atr\(this PriceSeries, int\)](VolatilityIndicators.Atr(thisPriceSeries,int).md 'TechnicalAnalysis\.Functions\.VolatilityIndicators\.Atr\(this TechnicalAnalysis\.Functions\.PriceSeries, int\)') | Computes the average true range\. | diff --git a/docs/functions/VolumeIndicators.Obv(thisPriceSeries).md b/docs/functions/VolumeIndicators.Obv(thisPriceSeries).md new file mode 100644 index 00000000..59d33db4 --- /dev/null +++ b/docs/functions/VolumeIndicators.Obv(thisPriceSeries).md @@ -0,0 +1,30 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions').[VolumeIndicators](VolumeIndicators.md 'TechnicalAnalysis\.Functions\.VolumeIndicators') + +## VolumeIndicators\.Obv\(this PriceSeries\) Method + +Computes on\-balance volume: the running total of volume, signed by the direction of the +close\. + +```csharp +public static TechnicalAnalysis.Functions.IndicatorSeries Obv(this TechnicalAnalysis.Functions.PriceSeries prices); +``` +#### Parameters + + + +`prices` [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries') + +The price series\. It must carry volumes, which only [FromOhlcv\(ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>, ReadOnlySpan<double>\)](PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md 'TechnicalAnalysis\.Functions\.PriceSeries\.FromOhlcv\(System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\, System\.ReadOnlySpan\\)') +supplies; substituting a constant volume would turn this into a signed bar counter\. + +#### Returns +[IndicatorSeries](IndicatorSeries.md 'TechnicalAnalysis\.Functions\.IndicatorSeries') +A bar\-aligned series that is warm from bar `0`, because the running total needs no +lookback\. The absolute level carries no meaning β€” it depends on where the series happens to +start β€” so only its direction and its divergence from price are read\. + +#### Exceptions + +[System\.InvalidOperationException](https://learn.microsoft.com/en-us/dotnet/api/system.invalidoperationexception 'System\.InvalidOperationException') +The price series carries no volumes\. \ No newline at end of file diff --git a/docs/functions/VolumeIndicators.md b/docs/functions/VolumeIndicators.md new file mode 100644 index 00000000..6d82a7a3 --- /dev/null +++ b/docs/functions/VolumeIndicators.md @@ -0,0 +1,20 @@ +#### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md 'Atypical\.TechnicalAnalysis\.Functions') +### [TechnicalAnalysis\.Functions](Atypical.TechnicalAnalysis.Functions.md#TechnicalAnalysis.Functions 'TechnicalAnalysis\.Functions') + +## VolumeIndicators Class + +Fluent volume indicators\. + +```csharp +public static class VolumeIndicators +``` + +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ VolumeIndicators + +### Remarks +The class name is TA\-Lib's own function group and never appears at a call site: these are +extension methods on [PriceSeries](PriceSeries.md 'TechnicalAnalysis\.Functions\.PriceSeries')\. + +| Methods | | +| :--- | :--- | +| [Obv\(this PriceSeries\)](VolumeIndicators.Obv(thisPriceSeries).md 'TechnicalAnalysis\.Functions\.VolumeIndicators\.Obv\(this TechnicalAnalysis\.Functions\.PriceSeries\)') | Computes on\-balance volume: the running total of volume, signed by the direction of the close\. | diff --git a/docs/functions/WclPriceResult.md b/docs/functions/WclPriceResult.md index 2ffdc209..21a30aec 100644 --- a/docs/functions/WclPriceResult.md +++ b/docs/functions/WclPriceResult.md @@ -9,7 +9,7 @@ Represents the result of the Weighted Close Price calculation\. public record WclPriceResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 WclPriceResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ WclPriceResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[WclPriceResult](WclPriceResult.md 'TechnicalAnalysis\.Functions\.WclPriceResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/WillRResult.md b/docs/functions/WillRResult.md index fdbe4742..4244a4ed 100644 --- a/docs/functions/WillRResult.md +++ b/docs/functions/WillRResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Williams' %R \(WillR\) indicator\. public record WillRResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 WillRResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ WillRResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[WillRResult](WillRResult.md 'TechnicalAnalysis\.Functions\.WillRResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/WmaResult.md b/docs/functions/WmaResult.md index b2375957..eddf72ae 100644 --- a/docs/functions/WmaResult.md +++ b/docs/functions/WmaResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Weighted Moving Average \(WMA\) indicat public record WmaResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 WmaResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ WmaResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[WmaResult](WmaResult.md 'TechnicalAnalysis\.Functions\.WmaResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/functions/ZigZagResult.md b/docs/functions/ZigZagResult.md index 5685c791..35d816fa 100644 --- a/docs/functions/ZigZagResult.md +++ b/docs/functions/ZigZagResult.md @@ -9,7 +9,7 @@ Represents the result of calculating the Zig Zag indicator\. public record ZigZagResult : TechnicalAnalysis.Common.SingleOutputResult, System.IEquatable ``` -Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') 🡒 [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') 🡒 [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') 🡒 ZigZagResult +Inheritance [System\.Object](https://learn.microsoft.com/en-us/dotnet/api/system.object 'System\.Object') β†’ [TechnicalAnalysis\.Common\.IndicatorResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.indicatorresult 'TechnicalAnalysis\.Common\.IndicatorResult') β†’ [TechnicalAnalysis\.Common\.SingleOutputResult](https://learn.microsoft.com/en-us/dotnet/api/technicalanalysis.common.singleoutputresult 'TechnicalAnalysis\.Common\.SingleOutputResult') β†’ ZigZagResult Implements [System\.IEquatable<](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1')[ZigZagResult](ZigZagResult.md 'TechnicalAnalysis\.Functions\.ZigZagResult')[>](https://learn.microsoft.com/en-us/dotnet/api/system.iequatable-1 'System\.IEquatable\`1') diff --git a/docs/guides/backtesting.md b/docs/guides/backtesting.md new file mode 100644 index 00000000..383eddae --- /dev/null +++ b/docs/guides/backtesting.md @@ -0,0 +1,846 @@ +# πŸ“‰ Backtesting with TaLibStandard + +A backtest is a machine for producing convincing numbers. Most of the work in building one goes into +making sure those numbers are not lies. The sample in +[`samples/TechnicalAnalysis.Samples.Backtesting`](../../samples/TechnicalAnalysis.Samples.Backtesting) is +a single-instrument, bar-by-bar engine whose design is organised around exactly two lies: + +1. **Look-ahead bias** β€” a strategy reading data it could not have had. Prevented *structurally* here: + the types make it impossible, and an attempt throws `LookAheadException` rather than reporting an + impossible Sharpe ratio. +2. **Misaligned indicators** β€” reading TA-Lib's output array as if element `k` described bar `k`. This + shifts every signal in time by `BegIdx` bars and is completely silent. The sample performs that + mapping in exactly one place. + +It ships with a 220-test suite that includes five deliberately cheating strategies, each proven to be +unable to profit from the cheat. + +--- + +## πŸ“ Table of contents + + +* [πŸ“‰ Backtesting with TaLibStandard](#-backtesting-with-talibstandard) + * [πŸ“ Table of contents](#-table-of-contents) + * [🏁 Run it](#-run-it) + * [Command line](#command-line) + * [What it prints](#what-it-prints) + * [πŸ—οΈ The engine model](#-the-engine-model) + * [The execution timeline](#the-execution-timeline) + * [Domain types](#domain-types) + * [πŸ”’ The no-look-ahead guarantee](#-the-no-look-ahead-guarantee) + * [Layer 1 β€” `IBarWindow`](#layer-1--ibarwindow) + * [Layer 2 β€” `IndicatorSeries`](#layer-2--indicatorseries) + * [Layer 3 β€” `IIndicatorSource`](#layer-3--iindicatorsource) + * [Layer 4 β€” the engine's own ordering](#layer-4--the-engines-own-ordering) + * [πŸ’Έ The cost model](#-the-cost-model) + * [πŸ“Š Metrics, formulas and annualisation](#-metrics-formulas-and-annualisation) + * [Return and growth](#return-and-growth) + * [Risk](#risk) + * [Risk-adjusted](#risk-adjusted) + * [Trade statistics](#trade-statistics) + * [Degenerate inputs](#degenerate-inputs) + * [✍️ Writing your own strategy](#-writing-your-own-strategy) + * [The contract](#the-contract) + * [A complete worked example](#a-complete-worked-example) + * [Registering it](#registering-it) + * [Rules of thumb](#rules-of-thumb) + * [πŸ“„ Using your own CSV](#-using-your-own-csv) + * [πŸ§ͺ Tests](#-tests) + * [⚠️ Limitations β€” read before believing any number](#-limitations--read-before-believing-any-number) + + +--- + +## 🏁 Run it + +```shell +# from the repository root β€” deterministic synthetic series, fully offline +dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -c Release +``` + +### Command line + +Verbatim `--help` output: + +```text +TaLibStandard - backtesting sample + +Runs four indicator strategies and a buy-and-hold baseline over the same price series, +then prints a side-by-side performance comparison. With no arguments it uses a deterministic +synthetic series, so it works completely offline. + +Usage: + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting [options] + +Options: + --csv Load bars from a CSV file instead of generating them. + Header required: Date,Open,High,Low,Close,Volume (any order). + --bars Number of synthetic bars to generate. Default: 1500. + --seed Seed of the synthetic series generator. Default: 20240101. + --capital Starting account equity. Default: 100000. + --commission-bps Commission per fill, in basis points of notional. Default: 5. + --slippage-bps Slippage per fill, in basis points of price. Default: 2. + --bars-per-year Annualisation constant for CAGR, volatility, Sharpe, Sortino, + Calmar. Default: 252 (daily bars, equity calendar). + --allow-short Permit short positions. Off by default, in which case a short + signal simply flattens the position. + --trade-log Print an excerpt of each strategy's round trips. + -h, --help Print this text and exit. + +Examples: + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -- --seed 7 --bars 3000 --allow-short + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -- --csv ./spy.csv --commission-bps 10 +``` + +`--help` exits `0`. An unknown option prints `Unknown option '--nope'.` followed by the usage text and +exits `1`. + +### What it prints + +Three blocks: a run-configuration header, one card per strategy (metrics table plus an ASCII equity +curve, and the trade log when `--trade-log` is given), then a side-by-side comparison. + +Real output from `dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -c Release` with no +arguments: + +```text +RUN CONFIGURATION + Data: deterministic synthetic series (seed 20240101) + Bars: 1,500 + Initial capital: 100,000.00 + Commission: 5 bp per fill + Slippage: 2 bp per fill + Position sizing: fixed fraction 100 % of equity + Short selling: disabled (short signals go flat) + Annualisation: 252 bars per year +``` + +```text +Metric SMA 20/50 RSI 14 reversion MACD 12/26/9 + 3xATR BBands 20/2 breakout Buy and hold +---------------------------------------------------------------------------------------------------------------------------------------- +Final equity 166,340.21 125,211.00 166,194.08 110,927.31 * 337,583.91 +Total return 66.34 % 25.21 % 66.19 % 10.93 % * 237.58 % +CAGR 8.93 % 3.85 % 8.92 % 1.76 % * 22.70 % +Annualised volatility 17.48 % * 4.39 % 15.96 % 12.92 % 22.76 % +Max drawdown 32.37 % * 6.70 % 20.42 % 30.37 % 19.89 % +Max DD duration (bars) 388 * 153 337 679 220 +Sharpe 0.58 0.88 0.61 0.20 * 1.01 +Sortino 0.84 * 1.53 0.91 0.29 1.50 +Calmar 0.28 0.58 0.44 0.06 * 1.14 +Trades 17 5 58 33 1 +Win rate 52.94 % * 100.00 % 39.66 % 27.27 % 100.00 % +Profit factor 2.19 * inf 1.54 1.16 inf +Average win 13,543.04 5,042.20 8,169.21 9,031.01 * 237,583.91 +Average loss -6,943.39 * 0.00 -3,477.08 -2,931.33 0.00 +Expectancy / trade 3,902.37 5,042.20 1,141.28 331.13 * 237,583.91 +Exposure 61.40 % 3.33 % 46.13 % 34.33 % 99.87 % + + * marks the best value of the row. +``` + +The strategy line-up is `SmaCrossoverStrategy(20, 50)`, `RsiMeanReversionStrategy()`, +`MacdTrendStrategy()`, `BollingerBreakoutStrategy()` and `BuyAndHoldStrategy()` β€” the baseline is always +last, so it reads as the reference column. Buy-and-hold pays exactly one round trip of the same costs as +everyone else, which is what makes it a fair benchmark rather than a free one. + +> **This is a synthetic series with 12% annual drift.** Buy-and-hold winning is a property of the data +> generator, not a finding about markets. Do not read the table as a strategy ranking. + +--- + +## πŸ—οΈ The engine model + +### The execution timeline + +For every bar `i`, in this exact order: + +```mermaid +sequenceDiagram + participant E as BacktestEngine + participant W as BarWindow + participant S as IStrategy + participant A as Account + + Note over E: bar i + E->>W: MoveTo(i) β€” bars 0..i readable, i+1.. throw + E->>A: fill the signal queued at close of bar i-1, at Open(i) + Note right of A: slippage + commission applied here + alt i == lastIndex and a position is open + E->>A: liquidate at Close(i) + end + E->>A: mark to market at Close(i) β†’ append EquityPoint + E->>S: Evaluate(window, position) + S-->>E: Signal for bar i+1 +``` + +Spelled out: + +1. The cursor moves to bar `i`. Bars `0..i` become readable; `i+1..` do not. +2. The signal produced at the close of bar `i-1` is filled at **`Open(i)`**, with slippage and + commission. +3. On the final bar, any position still open is liquidated at `Close(i)` when + `CloseOpenPositionAtEnd` is set (it is, by default), so the trade list and the final equity are fully + realised. +4. The account is marked to market at `Close(i)` and one `EquityPoint` is appended. +5. The strategy is asked for a signal, seeing bars `0..i` only. That signal is queued for bar `i+1`. On + the last bar the answer can no longer be executed, so it is discarded. + +The rule "a decision taken from bars `0..i` executes at `Open(i+1)`" is the entire causality model. +Everything below is machinery to make it impossible to violate. + +### Domain types + +| Type | Shape | Notes | +|------|-------|-------| +| `Bar` | `record (DateTime Timestamp, double Open, High, Low, Close, Volume)` | Plus `TypicalPrice`, `Range`, `IsWellFormed()` | +| `Signal` | `Hold \| EnterLong \| EnterShort \| Exit` | A **target state**, not a delta. `EnterLong` while already long is a no-op; while short it reverses in one fill | +| `Position` | `record (Side, Quantity, EntryPrice, EntryIndex, EntryTime, EntryCommission)` | `SignedQuantity`, `MarketValue(price)`, `UnrealizedProfit(price)` | +| `Trade` | one round trip | `GrossProfit`, `NetProfit` (= gross βˆ’ total commission), `ReturnOnNotional`, `BarsHeld`, `IsWin`, `IsLoss`. Both prices are realised fills, so slippage is already baked in; commission is not, and `NetProfit` subtracts it | +| `EquityPoint` | `(BarIndex, Timestamp, Price, Cash, SignedQuantity, Equity)` | `IsInPosition` drives the exposure metric | +| `BacktestOptions` | see [cost model](#-the-cost-model) | `Validate()` throws on anything unusable | +| `BacktestResult` | `(StrategyName, Options, EquityCurve, Trades, Metrics)` | What `BacktestEngine.Run` returns | + +--- + +## πŸ”’ The no-look-ahead guarantee + +Not "we were careful". Four layers, each of which independently makes cheating fail loudly. + +### Layer 1 β€” `IBarWindow` + +The only channel through which a strategy sees prices. + +```csharp +public interface IBarWindow +{ + int CurrentIndex { get; } + int Count { get; } // == CurrentIndex + 1 + Bar Current { get; } + Bar this[int index] { get; } + Bar Ago(int offset); // 0 = current, 1 = previous +} +``` + +Three properties do the work: + +* **`Count` reports the number of *visible* bars**, so the total length of the series never leaks. A + strategy cannot even discover how much future exists, let alone read it. The same applies to + `IndicatorSeries` β€” see Layer 2, because a strategy holds those too. +* **Any index above `CurrentIndex` throws `LookAheadException`**, even though a bar exists there. It is + thrown *in preference to* an out-of-range error, so "in the future" and "past the end" cannot be told + apart and binary-searched into an answer. +* **No member returns the backing collection**, so the future cannot be reached indirectly. + +`BarWindow.MoveTo` is `internal` β€” only the engine advances the cursor. A public cursor-mover would let a +strategy holding an `IBarWindow` walk itself forward. (The test project reaches it through an +`InternalsVisibleTo` declared in the sample's csproj, so tests can drive the cursor exactly as the engine +does.) + +`LookAheadException` is a distinct exception type deriving from `InvalidOperationException`, and **the +engine never catches it**. A cheating strategy takes the whole run down. + +### Layer 2 β€” `IndicatorSeries` + +The one place in the sample that applies TA-Lib's alignment rule: + +```text +output[k] describes bar (BegIdx + k) for k in [0, NBElement) +``` + +Elements at `k >= NBElement` are uninitialised zeros and carry no meaning. Reading the output array as if +element `k` described bar `k` shifts every signal backwards in time by `BegIdx` bars. `IndicatorSeries` +performs the mapping once, in its constructor, storing values by *bar index* alongside a per-bar +has-value flag. + +Its accessors then behave the way strategy code wants: + +| Member | Behaviour | +|--------|-----------| +| `TryGetValue(barIndex, out value)` | The preferred accessor. Reports the warm-up period without throwing | +| `TryGetPair(barIndex, out previous, out current)` | What a crossover test needs β€” both bars, or false | +| `HasValueAt(barIndex)` / `HasCurrent` | Warm-up test | +| `this[barIndex]` / `Current` | Throws `InvalidOperationException` on a warming-up bar, naming the first valid bar | +| all of the above | Throw `LookAheadException` when `barIndex > window.CurrentIndex` | + +An indicator value computed from tomorrow's close is exactly as fatal as reading tomorrow's close +directly, and it is treated the same way. + +**The metadata is guarded too**, which matters more than it sounds. A strategy receives its +`IndicatorSeries` objects from `Initialize`, *before* bar 0 is evaluated. If `Count` reported the full +bar count there, `_len = indicators.Sma(20).Count;` would hand the strategy the exact end date of the +backtest, and `if (bars.CurrentIndex >= _len - 20) return Signal.Exit;` is a one-line end-of-sample bias +that flattens before the final drawdown and inflates every risk metric β€” without ever reading a price. +So a bound series reports its metadata as of the current bar: + +| Member | Bound to a window | Unbound (`window: null`) | +|--------|-------------------|--------------------------| +| `Count` | `CurrentIndex + 1` β€” `0` inside `Initialize` | the full bar count | +| `BegIdx` | the first valid bar once the cursor reaches it, `-1` before that | as TA-Lib reported it | +| `NBElement` | the number of values that have already happened | as TA-Lib reported it | + +The full length is kept in a private field for bounds checking and is never observable. `TryGetPair` +tests causality *before* its range test for the same reason: a quiet `false` past the end plus a throw in +the future would be an oracle for the total length. + +Passing `window: null` disables the causality guard; that exists so the alignment arithmetic can be unit +tested in isolation. The engine always binds a window. + +### Layer 3 β€” `IIndicatorSource` + +Strategies declare indicators once, in `Initialize`, and **never receive raw price arrays**: + +```csharp +public interface IIndicatorSource +{ + IndicatorSeries Sma(int timePeriod); + IndicatorSeries Ema(int timePeriod); + IndicatorSeries Rsi(int timePeriod); + IndicatorSeries Atr(int timePeriod); + MacdSeries Macd(int fastPeriod, int slowPeriod, int signalPeriod); + BollingerBandSeries BollingerBands(int timePeriod, double deviationsUp, double deviationsDown); +} +``` + +`MacdSeries` is `(Line, Signal, Histogram)`; `BollingerBandSeries` is `(Upper, Middle, Lower)` β€” each +component a full `IndicatorSeries` with its own guard. + +Indicators are computed **once over the whole series** through `TAMath`, memoised by key, then re-indexed. +Computing over the whole series is safe precisely because every TA-Lib function is causal: the value at +bar `i` never depends on bar `i+1`. It is also far faster than a rolling recomputation, and numerically +identical to what a live feed would produce. + +A non-`Success` `RetCode` degrades to a zero-element series, so a misconfigured indicator makes the +strategy stay flat rather than trade on garbage. + +Note what the interface deliberately does *not* offer: any way to obtain the price arrays. + +### Layer 4 β€” the engine's own ordering + +The signal returned by `Evaluate` is **stored, not executed**. The fill price is read from the *next* +bar's open. Even a strategy with a perfect crystal ball could only act one bar late. + +**Proven, not asserted.** The test suite contains five cheating strategies β€” one reading a future bar, +one reading a future indicator value, one probing the bar window forward to find the series length, one +probing an `IndicatorSeries` the same way, and one reading `IndicatorSeries.Count` in `Initialize`. The +first four make the run throw `LookAheadException`; the fifth runs to completion and is asserted never to +observe a number equal to the total bar count. A sixth, recording strategy proves the window shows +exactly `i + 1` bars at bar `i`. + +--- + +## πŸ’Έ The cost model + +Two frictions, both expressed in **basis points** (1 bp = 0.01% = 0.0001), both always working against +the account. + +**Slippage** moves the fill price: + +```text +buy fills at price * (1 + SlippageBps / 10000) +sell fills at price * (1 - SlippageBps / 10000) +``` + +**Commission** is charged on every fill, on the *filled* notional: + +```text +commission = quantity * fillPrice * (CommissionBps / 10000) +``` + +A round trip therefore pays commission twice and slippage twice. + +**Position sizing** solves for a quantity that cannot overdraw the account: + +```text +quantity * fillPrice * (1 + commissionRate) = budget +``` + +so the cash leg *including* commission exactly consumes the budget. Cash stays non-negative for any +`PositionFraction` up to 1. + +| Option | Default | Meaning | +|--------|---------|---------| +| `InitialCapital` | `100_000` | Starting cash | +| `CommissionBps` | `5` | Per fill, on notional | +| `SlippageBps` | `2` | Per fill, on price. Must be below 10 000 bp or a sell would fill at or below zero | +| `Sizing` | `FixedFraction` | `FixedFraction` or `FixedCash` | +| `PositionFraction` | `1.0` | Fraction of equity per new position, in `(0, 1]`. No leverage | +| `PositionCash` | `10_000` | Cash notional per position when `Sizing = FixedCash`. Capped by equity | +| `AllowShort` | `false` | When false, `EnterShort` is **downgraded to `Exit`** β€” the strategy goes flat instead of short | +| `CloseOpenPositionAtEnd` | `true` | Liquidate at the last close so the trade list and final equity are realised | +| `BarsPerYear` | `252` | The annualisation constant. See below | +| `RiskFreeRate` | `0` | Annual, decimal fraction. De-annualised geometrically | + +`BacktestOptions.Validate()` throws `ArgumentException` on any non-finite or out-of-range value, and the +engine calls it in its constructor β€” so a nonsense configuration fails before it produces a plausible +equity curve. + +--- + +## πŸ“Š Metrics, formulas and annualisation + +**Notation.** `E(0..n-1)` is the equity curve, one point per bar, sampled at each bar's close *after* that +bar's fills. Per-bar simple returns are `r(t) = E(t)/E(t-1) - 1` for `t = 1..n-1`, so there are `n - 1` +of them. + +**Annualisation.** Every annualised figure uses `BarsPerYear`, which must match the bar interval of your +data. It is an explicit, configurable constant surfaced on both `BacktestOptions` and the resulting +`PerformanceMetrics` record, and settable with `--bars-per-year`, because getting it wrong silently +rescales half the table. + +| Data | `BarsPerYear` | +|------|--------------:| +| Daily, equity calendar | 252 | +| Daily, crypto (24/7) | 365 | +| Weekly | 52 | +| Monthly | 12 | +| 1-minute, equity calendar | 98 280 | + +Rates are scaled by `BarsPerYear`, standard deviations by `sqrt(BarsPerYear)` β€” the usual i.i.d. +square-root-of-time assumption, which **understates** risk when returns are autocorrelated (and they +are). The horizon in years is `(n - 1) / BarsPerYear`: it counts *bar intervals*, not equity points. + +### Return and growth + +| Metric | Formula | Notes | +|--------|---------|-------| +| **Total return** | `FinalEquity / InitialCapital - 1` | `0.25` means +25% | +| **CAGR** | `(FinalEquity / InitialCapital) ^ (1 / years) - 1`, `years = (BarCount - 1) / BarsPerYear` | `0` when fewer than two bars; `-1` when the account was wiped out | + +### Risk + +| Metric | Formula | Notes | +|--------|---------|-------| +| **Annualised volatility** | `stdev(r) * sqrt(BarsPerYear)` | *Sample* standard deviation, Bessel-corrected: divisor `n - 2` for the `n - 1` returns. Zero for fewer than two returns and for a perfectly flat curve | +| **Max drawdown** | `max over t of (peak(t) - E(t)) / peak(t)` where `peak(t) = max(E(0..t))` | Positive fraction. `0.30` = once 30% below the running high | +| **Max DD duration (bars)** | Greatest number of bars between a bar that set a running peak and the first later bar reaching that peak again | The recovery bar counts: `[100, 110, 99, 110]` is 2 bars, not 1. A drawdown still open on the last bar is measured up to that bar. A curve that only ever sets new highs is 0 | + +### Risk-adjusted + +| Metric | Formula | Notes | +|--------|---------|-------| +| **Sharpe** | `mean(r - rf) / stdev(r) * sqrt(BarsPerYear)` | `rf` is the per-bar risk-free rate, de-annualised **geometrically**: `(1 + RiskFreeRate) ^ (1 / BarsPerYear) - 1`. `stdev` is of the *raw* returns. Zero when volatility is zero or fewer than two returns exist | +| **Sortino** | `mean(r - rf) / downside(r) * sqrt(BarsPerYear)` with `downside(r) = sqrt( Ξ£ min(r(t) - rf, 0)Β² / count(r) )` | Note the denominator: the shortfalls are averaged over **all** returns, not only the negative ones. That is the Sortino–Satchell convention. Zero when no return falls below `rf` | +| **Calmar** | `Cagr / MaxDrawdown` | Same annualisation as CAGR. Zero when max drawdown is zero | + +### Trade statistics + +| Metric | Formula | Notes | +|--------|---------|-------| +| **Trades** | Count of completed round trips | A position open on the last bar is liquidated by the engine, so it counts | +| **Win rate** | `WinCount / TradeCount` | A win is `NetProfit > 0`. Exactly break-even is neither a win nor a loss | +| **Profit factor** | `Ξ£ positive NetProfit / \|Ξ£ negative NetProfit\|` | Both after commission. `0` with no trades; **`+∞`** with wins and no losses, rendered `inf` | +| **Average win / Average loss** | Mean `NetProfit` of the winners / of the losers | Average loss is reported as a **negative** number | +| **Expectancy / trade** | `Ξ£ NetProfit / TradeCount` | Identical to `WinRate Γ— AvgWin + (1 βˆ’ WinRate) Γ— AvgLoss` whenever no trade is exactly break-even | +| **Exposure** | `count(bars with a non-zero position) / BarCount` | `0.40` = in the market 40% of the time | +| **Peak equity** | Highest equity reached | | + +### Degenerate inputs + +**No member ever returns `NaN`.** The only non-finite value producible is `+∞` from `ProfitFactor`, which +is the conventional reading of "wins and no losses". Empty curve, single point, zero volatility, no +trades, all-losing, and wiped-out account are all defined and individually documented, and the test suite +asserts NaN-freeness across all five strategies as a theory. + +One specific choice worth naming: a wiped-out account (`E(t-1) <= 0`) yields a **flat** step rather than a +division by zero, because letting one `Infinity` into the return array poisons every downstream statistic. + +--- + +## ✍️ Writing your own strategy + +### The contract + +```csharp +public interface IStrategy +{ + string Name { get; } + string Description { get; } + void Initialize(IIndicatorSource indicators); + Signal Evaluate(IBarWindow bars, Position? position); +} +``` + +`Initialize` runs once before the first bar: resolve indicators and reset per-run state there, because +the same instance may be run over several series. `Evaluate` runs once per bar *after* the bar has +closed, with the window positioned on that bar, and the signal it returns is executed at the **open of +the next bar**. + +### A complete worked example + +Here is a full strategy, adapted from the shipped `RsiMeanReversionStrategy` and +`MacdTrendStrategy`. It buys a pullback in an uptrend β€” long only when price is above a slow SMA *and* +RSI crosses back up out of oversold β€” and exits on either an RSI target or a ratcheting ATR trailing +stop. + +```csharp +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// Buys a pullback inside an uptrend: long only when the close is above a slow SMA and RSI crosses back +/// up through the oversold level. Exits on an RSI target or a ratcheting ATR trailing stop, whichever +/// comes first. +/// +public sealed class TrendPullbackStrategy : IStrategy +{ + private readonly int _trendPeriod; + private readonly int _rsiPeriod; + private readonly double _oversoldLevel; + private readonly double _exitLevel; + private readonly int _atrPeriod; + private readonly double _atrMultiple; + + private IndicatorSeries? _trend; + private IndicatorSeries? _rsi; + private IndicatorSeries? _atr; + + // Per-run mutable state. Reset in Initialize, never in the constructor. + private double _trailingStop; + + /// Initializes a new instance of the class. + /// Period of the trend filter SMA, in bars. + /// Period of the RSI, in bars. + /// RSI level the entry crosses back up through. + /// RSI level that closes the position. + /// Period of the ATR driving the trailing stop, in bars. + /// How many ATRs below the close the stop sits. + /// A parameter is out of range. + public TrendPullbackStrategy( + int trendPeriod = 100, + int rsiPeriod = 14, + double oversoldLevel = 35.0, + double exitLevel = 65.0, + int atrPeriod = 14, + double atrMultiple = 2.5) + { + ArgumentOutOfRangeException.ThrowIfLessThan(trendPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(rsiPeriod, 1); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(oversoldLevel); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(exitLevel, oversoldLevel); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(exitLevel, 100.0); + ArgumentOutOfRangeException.ThrowIfLessThan(atrPeriod, 1); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(atrMultiple); + + _trendPeriod = trendPeriod; + _rsiPeriod = rsiPeriod; + _oversoldLevel = oversoldLevel; + _exitLevel = exitLevel; + _atrPeriod = atrPeriod; + _atrMultiple = atrMultiple; + } + + /// + public string Name => string.Format( + CultureInfo.InvariantCulture, + "Pullback SMA{0}/RSI{1} + {2}xATR", + _trendPeriod, + _rsiPeriod, + _atrMultiple); + + /// + public string Description => string.Format( + CultureInfo.InvariantCulture, + "Long when close > SMA{0} and RSI({1}) crosses back above {2}. Exit at RSI {3} or on a {4}x ATR trailing stop.", + _trendPeriod, + _rsiPeriod, + _oversoldLevel, + _exitLevel, + _atrMultiple); + + /// + public void Initialize(IIndicatorSource indicators) + { + ArgumentNullException.ThrowIfNull(indicators); + + _trend = indicators.Sma(_trendPeriod); + _rsi = indicators.Rsi(_rsiPeriod); + _atr = indicators.Atr(_atrPeriod); + + // The same instance may be reused across series, so per-run state resets here. + _trailingStop = 0.0; + } + + /// + public Signal Evaluate(IBarWindow bars, Position? position) + { + ArgumentNullException.ThrowIfNull(bars); + + if (_trend is null || _rsi is null || _atr is null) + { + throw new InvalidOperationException("Initialize must be called before Evaluate."); + } + + int i = bars.CurrentIndex; + double close = bars.Current.Close; + + // ---- exit first: a stop must be able to fire even when the entry filter is warming up ---- + if (position is not null) + { + if (_atr.TryGetValue(i, out double atr) && double.IsFinite(atr)) + { + // Ratchet: the stop only ever moves up. + _trailingStop = Math.Max(_trailingStop, close - (_atrMultiple * atr)); + } + + if (_trailingStop > 0.0 && close <= _trailingStop) + { + return Signal.Exit; + } + + if (_rsi.TryGetValue(i, out double rsiNow) && rsiNow >= _exitLevel) + { + return Signal.Exit; + } + + return Signal.Hold; + } + + // ---- entry: every input must have a value, or we do nothing ---- + // TryGetPair returns false during warm-up instead of throwing, which is why it is + // the preferred accessor in strategy code. + if (!_rsi.TryGetPair(i, out double previousRsi, out double currentRsi)) + { + return Signal.Hold; + } + + if (!_trend.TryGetValue(i, out double trend)) + { + return Signal.Hold; + } + + bool inUptrend = close > trend; + bool crossedUpOutOfOversold = previousRsi <= _oversoldLevel && currentRsi > _oversoldLevel; + + if (inUptrend && crossedUpOutOfOversold) + { + _trailingStop = 0.0; // re-arm for the new position + return Signal.EnterLong; + } + + return Signal.Hold; + } +} +``` + +Three things in there are worth copying rather than the rules themselves: + +* **Warm-up is handled by `TryGet*`, never by a try/catch.** During warm-up the answer is `Hold`, not an + exception and not a guessed value. +* **The trailing stop ratchets and is re-armed on entry**, and it lives in a field that `Initialize` + resets. Per-run state in a constructor is a bug waiting for the second series. +* **`double.IsFinite(atr)` is checked.** Given the ATR defect noted below, an unguarded stop at + `close - 2.5 Γ— ∞` is silently inert. Guarding non-finite indicator values is good practice regardless. + +### Registering it + +Drop the file in `Strategies/` and add it to the line-up: + +```csharp +// samples/TechnicalAnalysis.Samples.Backtesting/BacktestSampleRunner.cs +public static IReadOnlyList CreateStrategies() +{ + return + [ + new SmaCrossoverStrategy(20, 50), + new RsiMeanReversionStrategy(), + new MacdTrendStrategy(), + new BollingerBreakoutStrategy(), + new TrendPullbackStrategy(), // <- yours + new BuyAndHoldStrategy() // baseline stays last + ]; +} +``` + +Or drive the engine directly, without the console at all: + +```csharp +BacktestOptions options = new() { CommissionBps = 10, SlippageBps = 3, AllowShort = true }; +BacktestEngine engine = new(options); + +IReadOnlyList bars = CsvBarLoader.LoadFile("spy.csv"); +BacktestResult result = engine.Run(new TrendPullbackStrategy(), bars); + +Console.WriteLine($"{result.StrategyName}: {result.Metrics.Sharpe:0.00} Sharpe over {result.Metrics.TradeCount} trades"); +``` + +### Rules of thumb + +* **Emit a target state, not an instruction.** `EnterLong` while already long is a no-op; the engine will + not double up. +* **Never assume an indicator has a value.** Use `TryGetValue` / `TryGetPair` and return `Hold` otherwise. +* **A crossover needs both bars.** `TryGetPair` exists so you do not hand-roll `i` and `i - 1` and get the + boundary wrong. +* **Do not index the window past `CurrentIndex`.** It throws, on purpose. If you find yourself wanting to, + you have a look-ahead bug, not a missing feature. +* **Keep `Evaluate` pure apart from your own declared state.** The engine calls it exactly once per bar, + in order. +* **Exit logic before entry logic.** A stop that only evaluates after the entry filter passes is a stop + that does not work during warm-up. + +--- + +## πŸ“„ Using your own CSV + +```shell +dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -c Release -- \ + --csv ./spy.csv --capital 50000 --commission-bps 10 --bars-per-year 252 +``` + +`CsvBarLoader` requires a header row. Recognised column names are case-insensitive: + +| Column | Accepted names | Required | +|--------|----------------|----------| +| Date | `Date`, `Timestamp`, `Time`, `DateTime` | yes | +| Open | `Open` | yes | +| High | `High` | yes | +| Low | `Low` | yes | +| Close | `Close` | yes | +| Volume | `Volume` | no β€” defaults to `0` | + +**Column order does not matter and extra columns are ignored**, so a broker export carrying an +`Adj Close` column loads unchanged. Numbers and dates parse with the invariant culture; dates are read +with `AdjustToUniversal | AssumeUniversal`, so a naive timestamp is treated as UTC. A malformed field +raises a `FormatException` naming the line number and the column. + +A minimal file: + +```csv +Date,Open,High,Low,Close,Volume +2024-01-02,100.00,101.50,99.20,100.80,1200000 +2024-01-03,100.90,102.30,100.10,101.90,980000 +2024-01-04,101.80,103.00,101.20,102.40,1100000 +2024-01-05,102.50,102.90,100.70,101.10,1350000 +2024-01-08,101.00,101.80,99.90,100.20,900000 +``` + +Verified: that file loads and runs, printing `Bars: 5` with the requested capital and commission. + +`CsvBarLoader.Validate(bars)` is run automatically and reports up to five data warnings above the +results β€” bars that are not internally consistent (`High` not the maximum, a non-positive price, a +negative volume) and timestamps that are not strictly ascending. It **warns**; it does not refuse. Decide +for yourself whether a warning invalidates the run. + +Two things to get right before trusting the output: + +* **`--bars-per-year` must match your bar interval.** Daily equity bars are 252. Feeding hourly bars with + the default 252 will not error; it will just print a CAGR, a volatility and a Sharpe that are all wrong + by a factor of about five. +* **Adjust for splits and dividends yourself.** The loader does not, and an unadjusted split looks + exactly like a βˆ’50% day to every indicator in the library. + +--- + +## πŸ§ͺ Tests + +```shell +dotnet test tests/TechnicalAnalysis.Samples.Backtesting.UnitTests -c Release +``` + +Verified: `Passed! - Failed: 0, Passed: 220, Skipped: 0, Total: 220`. + +What the suite actually pins, since a passing count on its own means nothing: + +* **Metric formulas** against a hand-computed fixture `[100, 110, 99, 108.9]` whose arithmetic is written + out in the test class docs, asserted against exact analytic values β€” Sharpe `= sqrt(84)/2`, volatility + `= 0.2 * sqrt(84)`, max drawdown `= 0.10`, drawdown duration `= 2 bars` β€” at a documented `1e-9` + tolerance. +* **Look-ahead**: five cheating strategies (future bar, future indicator value, forward probing of the + bar window, forward probing of an `IndicatorSeries`, and reading `IndicatorSeries.Count` in + `Initialize`) β€” the first four proven to throw `LookAheadException`, the fifth proven never to see the + total bar count; plus a recording strategy proving the window shows exactly `i + 1` bars at bar `i`. +* **Costs**: commission and slippage checked against closed-form final equity for a known single round + trip, separately and together. +* **Alignment**: indicator-to-bar mapping proven against a raw `TAMath.Sma` call, *including* an explicit + assertion that the naive `raw.Real[barIndex]` read gives a different answer. +* **Baseline**: buy-and-hold equals the underlying open-to-close return net of exactly one round trip of + costs. +* **Degenerate inputs**: empty series, 10 bars against SMA(50)/MACD/BBands(20), a single bar, 300 flat + bars, a 99.9% collapse β€” all asserted NaN-free and sane, as a theory across all five strategies. + +--- + +## ⚠️ Limitations β€” read before believing any number + +This is a teaching engine. Everything below is true of the implementation as it stands. + +**Execution model** + +* **No intrabar fills.** Every fill happens at a bar's `Open` (or, for the final liquidation, its + `Close`). A stop or a limit that would have triggered inside a bar is not simulated β€” the sample's ATR + stop is evaluated on *closes* and fills at the next open, which is realistic for a + "check-at-the-close" system and pessimistic for a resting stop order. +* **No partial fills.** An order is filled entirely or not at all. +* **No market-impact model.** Slippage is a fixed number of basis points, independent of your size and of + the bar's volume. Size a position at 100% of a large account and the model will happily pretend the + market did not notice. +* **No order types.** There are no limit, stop, stop-limit, or MOC orders β€” only "be long", "be short", + "be flat" from the next open. +* **No queue position, no bid/ask spread as such.** The slippage constant is standing in for both. +* **Fills never fail.** No halts, no limit-up/limit-down, no gaps you cannot trade through. + +**Portfolio model** + +* **Single instrument.** One `IStrategy` runs over one `IReadOnlyList`. There is no portfolio, no + cross-sectional ranking, no correlation, no capital allocation across strategies. +* **One position at a time.** No pyramiding, no scaling in or out. `EnterLong` while long is a no-op. +* **No margin, no borrow costs, no financing.** Shorting is free: no locate, no borrow fee, no rebate, no + hard-to-borrow constraint. Long positions pay no financing either. `PositionFraction` is capped at 1 + precisely because there is no leverage model to make anything above 1 meaningful. +* **No dividends, no corporate actions, no taxes.** Total return only, and only if your input series is + already adjusted. +* **No currency.** Everything is in one nominal unit. + +**Statistical model** + +* **Square-root-of-time annualisation**, which understates risk for autocorrelated returns. +* **`BarsPerYear` is your responsibility.** Nothing infers it from the timestamps. +* **One path, one parameter set.** There is no walk-forward, no cross-validation, no parameter sweep, no + Monte Carlo, no bootstrapped confidence interval on the Sharpe ratio. A single backtest over a single + series is an anecdote. +* **No survivorship-bias handling**, because there is no universe β€” but if you point this at a + hand-picked ticker that still exists today, that bias is yours and the engine cannot see it. + +**Data** + +* **The default series is synthetic**, seeded geometric Brownian motion with 12% annual drift and 22% + annual volatility, gapped opens, half-normal wicks and weekday timestamps. It is always well-formed, + never halts and never gaps 20%. It is there so the sample runs offline, not so it resembles a market. +* **CSV input is validated but not repaired.** Warnings are printed; the run proceeds. + +**Library defects that affect results** + +* **`TAFunc.Atr` never divides its running average** + (`src/TechnicalAnalysis.Functions/Atr/TAFunc.cs`), so ATR grows by a factor of `period - 1` on every + bar after the second output. Measured on a series whose true range is exactly `2.0` every bar, + `Atr(…, 14)` returns `2, 2, 26.142857, 340, 4420.1429, 57462, 747006.14, 9711080, …` β€” each value + exactly 13Γ— (`period - 1`) the previous one; only the first two are correct. On the sample's own + 1 500-bar synthetic series, `Atr(…, 14)` is `2.32` at bar 14, `2.30` at bar 15, `29.97` at bar 16, + `4.40e+110` at bar 114 and `+∞` from bar 300 onwards. Consequence here: `MacdTrendStrategy`'s ATR + trailing stop is correctly implemented but **effectively inert** beyond the first handful of bars, + because the stop sits at `close - 3 Γ— ∞`. Every MACD exit in the table above is therefore a + signal-line crossing, not a stop, and **when the defect is fixed the MACD column will change.** The + sample deliberately does not clamp the value or pin a test to the buggy behaviour, because either + would encode a library defect into a sample. +* **`TA_INT_EMA` seeds itself low** (sums `period - 1` values, divides by `period`), which affects Ema, + Macd, MacdExt, MacdFix, Dema, Tema, T3, Apo, Ppo and Trix. The error decays with the smoothing factor, + so it distorts the bars just after warm-up rather than the steady state. +* **`TAFunc.Rsi` returns `NaN` on a perfectly flat series** (no zero guard on `prevGain + prevLoss`). + +--- + +## Related + +* [Getting started](getting-started.md) β€” `RetCode`, `BegIdx`/`NBElement` and the alignment rule from + first principles, with a hand-checkable worked example. +* [Real-time streaming](real-time-streaming.md) β€” the same alignment rule against a live feed, and the + window-recompute-versus-incremental trade-off. +* [Indicator catalog](../indicators/README.md) β€” every entry point, signature and output. +* [TradingView integration](tradingview-integration.md) β€” parity caveats if you are reconciling these + results against Pine Script. diff --git a/docs/guides/benchmarks.md b/docs/guides/benchmarks.md new file mode 100644 index 00000000..5de62332 --- /dev/null +++ b/docs/guides/benchmarks.md @@ -0,0 +1,496 @@ +# ⚑ Performance benchmarks + +TaLibStandard ships a BenchmarkDotNet suite at +[`benchmarks/TechnicalAnalysis.Benchmarks`](../../benchmarks/TechnicalAnalysis.Benchmarks). It exists to +answer four questions with numbers instead of intuition: + +1. How long does an indicator take, and how does that scale with series length? +2. What does the ergonomic `TAMath` API cost compared with the allocation-free `TAFunc` API? +3. What does the generic-math candlestick design cost on `float` and `decimal` versus `double`? +4. How does the managed port compare with the original TA-Lib C library? + +Only the fourth needs anything installed. The other three run on a clean clone, offline, against +deterministic synthetic data. + +> **Results policy.** This page publishes only measurements that were actually taken, each labelled with +> the machine, the OS, the .NET version and the exact command that produced it. Everything else is a +> command you can run yourself. See [Measured results](#-measured-results) and +> [What has *not* been measured](#what-has-not-been-measured). + +--- + +## πŸ“ Table of contents + + +* [⚑ Performance benchmarks](#-performance-benchmarks) + * [πŸ“ Table of contents](#-table-of-contents) + * [🏁 Quick start](#-quick-start) + * [πŸ”¬ What is measured](#-what-is-measured) + * [The two API paths](#the-two-api-paths) + * [The suites](#the-suites) + * [The synthetic data](#the-synthetic-data) + * [▢️ Running the suite](#-running-the-suite) + * [Everything](#everything) + * [One category](#one-category) + * [One filter](#one-filter) + * [Switch reference](#switch-reference) + * [Correctness gate: `--selfcheck`](#correctness-gate---selfcheck) + * [πŸ”Œ Enabling the native TA-Lib C comparison](#-enabling-the-native-ta-lib-c-comparison) + * [Install the native library](#install-the-native-library) + * [How discovery works](#how-discovery-works) + * [Correctness before speed](#correctness-before-speed) + * [πŸ“– Reading BenchmarkDotNet output](#-reading-benchmarkdotnet-output) + * [The columns](#the-columns) + * [Exported files](#exported-files) + * [πŸ“Š Measured results](#-measured-results) + * [SMA β€” `TAFunc` versus `TAMath`](#sma--tafunc-versus-tamath) + * [RSI β€” `TAFunc` versus `TAMath`](#rsi--tafunc-versus-tamath) + * [What these two tables do and do not say](#what-these-two-tables-do-and-do-not-say) + * [What has *not* been measured](#what-has-not-been-measured) + * [⚠️ Methodology caveats](#-methodology-caveats) + + +--- + +## 🏁 Quick start + +From the repository root: + +```shell +# list every benchmark without running anything +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --list flat + +# prove every benchmark computes something valid (fast, not a measurement) +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --selfcheck + +# one real measurement, roughly 40 seconds +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --filter "*OverlapStudiesBenchmarks.Sma*" --job Short +``` + +`-c Release` is not optional. BenchmarkDotNet refuses to run a Debug build, and it is right to. + +The suite prints a banner before it does anything, naming the runtime, the OS, whether Server GC is on, +and whether the native TA-Lib C library was found. Keep that banner with any numbers you quote. + +--- + +## πŸ”¬ What is measured + +There are **119 benchmark methods** in five always-runnable classes, each parameterised over three series +lengths (`1_000`, `10_000`, `100_000`) and each carrying `[MemoryDiagnoser]`. A sixth class, +`NativeComparisonBenchmarks`, adds 16 more but is offered only when the native library is present. + +### The two API paths + +Every indicator in the three indicator suites appears **twice**, and the suffix is the whole point: + +| Suffix | API | Output buffers | What the number means | +|--------|-----|----------------|-----------------------| +| `_TAFunc` | `TAFunc.Sma(…, ref outReal)` | Supplied by the caller, allocated in `[GlobalSetup]` | The algorithm alone. Allocates ~0 B per call. | +| `_TAMath` | `TAMath.Sma(…) -> SmaResult` | Allocated per call by the library | The algorithm **plus** one output array and one result record per call. | + +That is not a fair fight and is not meant to be. It is a price tag: it tells you what the convenient API +costs when you call it in a loop, so you can decide whether the low-level API is worth the ceremony in +your hot path. For a once-per-bar recompute it never is. + +### The suites + +| Class | Benchmarks | Category tag | Contents | +|-------|-----------:|--------------|----------| +| `OverlapStudiesBenchmarks` | 22 | `OverlapStudies` | Sma, Ema, Wma, Dema, Tema, Trima, Kama, T3, BollingerBands, MidPoint, Sar | +| `MomentumBenchmarks` | 24 | `Momentum` | Rsi, Macd, Stoch, StochRsi, Adx, Cci, Mfi, WillR, Ppo, Roc, UltOsc, Aroon | +| `VolatilityVolumeBenchmarks` | 20 | `VolatilityVolume` | Atr, Natr, TrueRange, Obv, Ad, AdOsc, StdDev, Variance, Correl, Beta | +| `CandlePatternBenchmarks` | 39 | `CandlePatterns` | 13 patterns Γ— `double` / `float` / `decimal` | +| `PrecisionBenchmarks` | 14 | `Precision` | Sma, Ema, Rsi, Macd, BollingerBands, Atr, Correl on `double` versus `float` | +| `NativeComparisonBenchmarks` | 16 | `NativeComparison` | Managed versus C for Sma, Ema, Rsi, Macd, Bbands, Atr, Adx, Stoch | + +Cross-cutting tags let you slice differently: `TAFunc`, `TAMath`, `double`, `float`, `decimal`, +`Managed`, `Native`. Every name is a constant in +[`Benchmarks/BenchmarkCategories.cs`](../../benchmarks/TechnicalAnalysis.Benchmarks/Benchmarks/BenchmarkCategories.cs), +so you never have to guess at spelling. + +**What the precision suite actually shows.** `TAMath`'s `float[]` overloads widen their input into a +freshly allocated `double[]` and then call the same `double` kernel; outputs are always `double[]`. So +`*_Float` measures *the widening copy plus the identical algorithm* β€” it is a cost comparison, not an +accuracy comparison, and it should come out slower, not faster. The candlestick suite is different: +`TACandle` is generic over `IFloatingPoint`, so `Doji_Decimal` really does run decimal arithmetic +through the same source. + +### The synthetic data + +Generated by +[`Data/MarketDataGenerator.cs`](../../benchmarks/TechnicalAnalysis.Benchmarks/Data/MarketDataGenerator.cs) +with a fixed default seed of **20240217**, using a discretised geometric Brownian motion: 8% annual +drift, 25% annual volatility, `dt = 1/252`. Opens carry an overnight gap, highs and lows get independent +exponential wicks, volume is log-normal and correlated with `|return|`. A second instrument, correlated +at ρ = 0.65, feeds `Correl` and `Beta`. + +Two details matter for interpreting results: + +* Prices are rounded to **4 decimals at generation**, so the `decimal[]` projection is an exact + representation of the `double[]` one. Only the `float[]` projection loses information. Precision + comparisons are therefore pure cost comparisons, with no accuracy confound. +* Random numbers come from an in-project xoshiro256\*\* seeded through SplitMix64, not `System.Random`, + because the BCL does not guarantee seed-to-sequence stability across runtime versions. Same seed, same + series, forever. + +Generation happens only inside `[GlobalSetup]`, never inside a measured method, and results are cached +per `(length, seed)`. + +--- + +## ▢️ Running the suite + +### Everything + +```shell +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --filter "*" +``` + +**Budget for this.** 119 methods Γ— 3 lengths with the default job is on the order of an hour on a fast +laptop. Prefer a category or a filter, and add `--job Short` when you want a quick read rather than a +publishable one. + +### One category + +```shell +# one suite +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --anyCategories VolatilityVolume + +# only the allocation-free kernels, across every suite +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --anyCategories TAFunc + +# the decimal candlestick benchmarks only β€” both tags must match +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --allCategories CandlePatterns decimal +``` + +Verified selection sizes (from `--list flat` with the same filters, so no measurement time was spent): + +| Filter | Benchmarks selected | +|--------|--------------------:| +| *(none)* | 119 | +| `--anyCategories VolatilityVolume` | 20 | +| `--anyCategories TAFunc` | 33 | +| `--allCategories CandlePatterns decimal` | 13 | + +### One filter + +`--filter` takes glob patterns against the fully-qualified benchmark name, and accepts several at once: + +```shell +# every RSI benchmark anywhere in the suite +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --filter "*Rsi*" + +# one class +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --filter "*PrecisionBenchmarks*" + +# two specific pairs, quickly +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --filter "*OverlapStudiesBenchmarks.Sma*" "*MomentumBenchmarks.Rsi*" --job Short +``` + +### Switch reference + +Everything BenchmarkDotNet understands is passed straight through. The ones that matter here: + +| Switch | Effect | +|--------|--------| +| `--list flat` | Print benchmark names and exit. Free; use it to check a filter before spending an hour. | +| `--filter …` | Select by fully-qualified name. Several globs allowed. | +| `--anyCategories …` | Select benchmarks carrying **any** of the tags. | +| `--allCategories …` | Select benchmarks carrying **all** of the tags. | +| `--job Short` | 3 warmup + 3 target iterations. A real but low-confidence measurement. | +| `--job Dry` | One cold iteration. **Not a measurement** β€” a smoke test. Allocation numbers are still meaningful. | +| `--runtimes …` | Run the same code on several runtimes. | +| `--artifacts ` | Redirect exported reports. | +| `--selfcheck` | Project-specific. Correctness gate, described below. | + +The shared config declares **no job** on purpose, so `--job Short` adds exactly one job rather than +multiplying an already-declared one. See +[`Configuration/TaLibBenchmarkConfig.cs`](../../benchmarks/TechnicalAnalysis.Benchmarks/Configuration/TaLibBenchmarkConfig.cs). + +### Correctness gate: `--selfcheck` + +```shell +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- --selfcheck +``` + +This invokes every benchmark once at `Length = 1000`, outside BenchmarkDotNet, and asserts that each one +reports `RetCode.Success` (or a successful `IndicatorResult` with `NBElement > 0`, or native code `0`). + +It exists because a benchmark that quietly measures a validation-failure path is worse than no benchmark +at all: the fast path out of an argument check is *very* fast. **It is a correctness gate, not a +measurement**, and it prints no timings. + +Real output on this machine: + +```text +Self check: invoking every benchmark once with Length = 1000. + + CandlePatternBenchmarks OK + MomentumBenchmarks OK + OverlapStudiesBenchmarks OK + PrecisionBenchmarks OK + VolatilityVolumeBenchmarks OK + +Self check passed: 119 benchmark method(s) all reported success. +``` + +--- + +## πŸ”Œ Enabling the native TA-Lib C comparison + +`NativeComparisonBenchmarks` runs the managed `TAFunc` path head to head against the original TA-Lib C +library through P/Invoke. The suite has **no** native dependency: when the library is missing, the class +is removed from the type set handed to `BenchmarkSwitcher`, so `--list` and `--filter "*"` never offer +something that cannot work, and every other suite runs normally. + +If it is missing, the banner says so loudly: + +```text + NATIVE TA-LIB: NOT AVAILABLE - the managed-versus-C comparison is DISABLED. + Probe result : probed 19 candidate name(s)/path(s), none loaded. + Everything else runs normally; the suite has no native dependency by design. +``` + +### Install the native library + +| Platform | Command | +|----------|---------| +| macOS (Homebrew) | `brew install ta-lib` β€” installs `/opt/homebrew/lib/libta-lib.dylib` on Apple silicon, `/usr/local/lib/…` on Intel | +| Debian / Ubuntu | `sudo apt-get install libta-lib0 libta-lib-dev` when packaged, otherwise build from source | +| Linux (source) | `./configure --prefix=/usr && make && sudo make install`, then `sudo ldconfig` | +| Windows | Put `ta-lib.dll` (or `ta_libc_cdr.dll`) on `PATH` or next to the built executable | +| Any OS | `export TALIB_NATIVE_LIBRARY=/full/path/to/libta-lib.dylib` | + +### How discovery works + +[`Interop/NativeTaLib.cs`](../../benchmarks/TechnicalAnalysis.Benchmarks/Interop/NativeTaLib.cs) registers +a `DllImportResolver` in a static constructor and probes, in order: + +1. `TALIB_NATIVE_LIBRARY`, if set β€” this always wins, and is the escape hatch for an unusual install. +2. Eight bare names, letting the platform loader apply its own `lib` prefix and `.so` / `.dylib` / `.dll` + suffix rules: `ta-lib`, `ta_lib`, `libta-lib`, `libta_lib`, `ta_libc`, `ta_libc_cdr`, + `libta-lib.so.0`, `libta_lib.so.0`. +3. Eleven absolute paths covering the default Homebrew, MacPorts and autotools prefixes. + +Nineteen candidates in total β€” which is the `probed 19 candidate name(s)/path(s)` in the banner above. A +candidate that loads but exports no `TA_Initialize` is rejected rather than trusted. `TA_Initialize` runs +once on first use and `TA_Shutdown` is registered on `ProcessExit`. Every probe failure is caught: +`IsAvailable`, `ResolvedName` and `Diagnostics` never throw. + +Only ten C entry points are bound β€” `TA_Initialize`, `TA_Shutdown`, `TA_SMA`, `TA_EMA`, `TA_RSI`, +`TA_MACD`, `TA_BBANDS`, `TA_ATR`, `TA_ADX`, `TA_STOCH` β€” because those are the signatures the author was +confident about from upstream `ta_func.h`. Binding a signature wrongly does not fail to compile; it +corrupts memory at runtime, so the conservative choice is deliberate. + +### Correctness before speed + +`[GlobalSetup]` runs both implementations once and asserts they agree β€” identical `outBegIdx` and +`outNBElement`, and every value within a `1e-9` relative tolerance, with explicit NaN handling β€” **before +anything is timed**. All three MACD series, all three Bollinger series and both Stochastic series are +checked. A "faster" result therefore cannot come from computing the wrong thing. + +Once you have the library: + +```shell +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks -c Release -- \ + --anyCategories NativeComparison --job Short +``` + +The class is grouped by indicator with the managed method as the baseline, so the `Ratio` column reads as +native Γ· managed. + +--- + +## πŸ“– Reading BenchmarkDotNet output + +### The columns + +| Column | Meaning | How to use it | +|--------|---------|---------------| +| **Mean** | Arithmetic mean of all measured iterations, per operation. | The headline number. Compare within one table, never across machines. | +| **Error** | Half of the 99.9% confidence interval of the mean. | If two means differ by less than the sum of their errors, you have not measured a difference. | +| **StdDev** | Standard deviation of the iteration times. | Large relative to the mean means a noisy machine: close things, re-run. | +| **Op/s** | Operations per second, i.e. `1 / Mean`. | Easier to reason about for throughput sizing ("how many series per second?"). | +| **Ratio** | Mean of the ratio distribution against the `Baseline = true` method of the same *logical group*. Rendered as *trend* text: `1.48x slower`. | **Only `NativeComparisonBenchmarks` emits it**, and there it is a genuine like-for-like comparison. See the note below. | +| **RatioSD** | Standard deviation of that ratio distribution. | A ratio with a large SD is not a finding. | +| **Gen0 / Gen1 / Gen2** | GC collections per 1000 operations. | A non-zero `Gen2` on a per-call path is a design smell, not a rounding error. | +| **Allocated** | Managed bytes allocated per single operation. | Stable, machine-independent, and the most portable thing in the table. | +| **Alloc Ratio** | Allocation against the baseline of the same logical group. | Reads as `202.60x more`, and is exact β€” allocation is not sampled. Same scope as `Ratio`. | + +`Length` is the `[Params]` value: the number of bars fed to the indicator. + +> **Why most suites have no `Ratio` column.** BenchmarkDotNet's default logical group is (Job, Params), so +> a single `[Benchmark(Baseline = true)]` in a class ratios *every* method in that class against that one +> method. In `OverlapStudiesBenchmarks` that would print `Ema_TAMath … 3.73x slower` β€” against `Sma_TAFunc`, +> not against `Ema_TAFunc`, which is the comparison the row looks like it is making. (The real +> `TAMath`/`TAFunc` delta for EMA at `Length = 1000` was `0.82x` in the same run.) Grouping per indicator +> does not fix it either: `BenchmarkLogicalGroupRule.ByCategory` keys on the *whole* category set, and the +> `TAFunc` / `TAMath` categories put the two halves of each pair in different groups. +> +> Rather than publish a column that is wrong for 21 rows out of 22, those suites declare no baseline at +> all. **Read the two rows of the same indicator and divide `Mean` yourself.** +> `NativeComparisonBenchmarks` is the one suite whose categories are per-indicator and mutually exclusive, +> so it carries `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]` and eight baselines, and its +> `Ratio` column means exactly what it says. + +Expect timings to scale roughly linearly in `Length` for the simple kernels and allocation to scale +exactly linearly: an output array is `8 Γ— Length` bytes, plus a fixed record header. + +### Exported files + +Two exporters are configured, and both land in `BenchmarkDotNet.Artifacts/results` **next to the built +executable** (not in the repository root β€” the artifacts path is anchored to `AppContext.BaseDirectory` +precisely so a run from the repo root does not litter it): + +* `*-report-github.md` β€” GitHub-flavoured markdown, paste-ready into an issue or a release note. +* `*-report-full.json` β€” machine readable, for tracking regressions between releases. + +Override with `--artifacts `. + +--- + +## πŸ“Š Measured results + +Everything in this section was produced by a command that was actually run. Nothing is extrapolated. + +**Environment** (printed by BenchmarkDotNet itself): + +```text +BenchmarkDotNet v0.15.8, macOS Tahoe 26.5.2 (25F84) [Darwin 25.5.0] +Apple M1 Max, 1 CPU, 10 logical and 10 physical cores +.NET SDK 10.0.301 + [Host] : .NET 10.0.9 (10.0.9, 10.0.926.27113), Arm64 RyuJIT armv8.0-a + ShortRun : .NET 10.0.9 (10.0.9, 10.0.926.27113), Arm64 RyuJIT armv8.0-a + +Job=ShortRun IterationCount=3 LaunchCount=1 +WarmupCount=3 +``` + +**Exact command:** + +```shell +dotnet run --project benchmarks/TechnicalAnalysis.Benchmarks/TechnicalAnalysis.Benchmarks.csproj \ + -c Release --no-build -- \ + --filter "*OverlapStudiesBenchmarks.Sma*" "*MomentumBenchmarks.Rsi*" --job Short +``` + +Total wall time: 94.37 s for 12 benchmarks. + +> **One column has since been removed.** These tables are the verbatim output of that run, taken while +> `OverlapStudiesBenchmarks` and `MomentumBenchmarks` still declared a class-wide +> `[Benchmark(Baseline = true)]`. Because the filter reduced each class to a single indicator, the +> `Ratio` / `RatioSD` / `Alloc Ratio` columns below *are* `TAMath` against `TAFunc` for the same +> indicator, which is what they appear to be β€” the timings and the ratios are all real. Re-run the same +> command today and you will get the same rows without those three columns, because the misleading +> baselines were removed (see the note in [Reading BenchmarkDotNet output](#the-columns)). The ratio is +> then simply `Mean(TAMath) / Mean(TAFunc)` from the two adjacent rows. + +### SMA β€” `TAFunc` versus `TAMath` + +| Method | Categories | Length | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|----------- |---------------------- |------- |-----------:|-----------:|----------:|----------:|-------------:|--------:|-------:|-------:|-------:|----------:|----------------:| +| Sma_TAFunc | TAFunc,OverlapStudies | 1000 | 2.432 us | 0.2696 us | 0.0148 us | 411,113.3 | baseline | | - | - | - | 40 B | | +| Sma_TAMath | TAMath,OverlapStudies | 1000 | 2.956 us | 0.1263 us | 0.0069 us | 338,278.3 | 1.22x slower | 0.01x | 0.1068 | - | - | 8104 B | 202.60x more | +| Sma_TAFunc | TAFunc,OverlapStudies | 10000 | 25.529 us | 4.8932 us | 0.2682 us | 39,170.7 | baseline | | - | - | - | 40 B | | +| Sma_TAMath | TAMath,OverlapStudies | 10000 | 27.660 us | 7.3039 us | 0.4004 us | 36,153.1 | 1.08x slower | 0.02x | 1.0681 | - | - | 80104 B | 2,002.60x more | +| Sma_TAFunc | TAFunc,OverlapStudies | 100000 | 251.581 us | 4.0547 us | 0.2223 us | 3,974.9 | baseline | | - | - | - | 40 B | | +| Sma_TAMath | TAMath,OverlapStudies | 100000 | 372.630 us | 60.8805 us | 3.3371 us | 2,683.6 | 1.48x slower | 0.01x | 8.7891 | 8.7891 | 8.7891 | 800118 B | 20,002.95x more | + +### RSI β€” `TAFunc` versus `TAMath` + +| Method | Categories | Length | Mean | Error | StdDev | Op/s | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|----------- |---------------- |------- |-----------:|------------:|-----------:|----------:|-------------:|--------:|--------:|--------:|--------:|----------:|----------------:| +| Rsi_TAFunc | TAFunc,Momentum | 1000 | 4.835 us | 0.6293 us | 0.0345 us | 206,810.4 | baseline | | - | - | - | 40 B | | +| Rsi_TAMath | TAMath,Momentum | 1000 | 5.433 us | 0.7055 us | 0.0387 us | 184,059.2 | 1.12x slower | 0.01x | 0.1068 | - | - | 8104 B | 202.60x more | +| Rsi_TAFunc | TAFunc,Momentum | 10000 | 49.459 us | 10.2223 us | 0.5603 us | 20,218.8 | baseline | | - | - | - | 40 B | | +| Rsi_TAMath | TAMath,Momentum | 10000 | 51.421 us | 4.6344 us | 0.2540 us | 19,447.5 | 1.04x slower | 0.01x | 1.0376 | - | - | 80104 B | 2,002.60x more | +| Rsi_TAFunc | TAFunc,Momentum | 100000 | 516.477 us | 58.3711 us | 3.1995 us | 1,936.2 | baseline | | - | - | - | 40 B | | +| Rsi_TAMath | TAMath,Momentum | 100000 | 639.357 us | 207.9159 us | 11.3966 us | 1,564.1 | 1.24x slower | 0.02x | 11.7188 | 11.7188 | 11.7188 | 800157 B | 20,003.92x more | + +### What these two tables do and do not say + +**Do say:** + +* Both kernels are linear in series length. SMA goes 2.4 β†’ 25.5 β†’ 251.6 Β΅s across 1k β†’ 10k β†’ 100k; + RSI goes 4.8 β†’ 49.5 β†’ 516.5 Β΅s. Nothing quadratic is hiding in there. +* `TAFunc` with caller-supplied buffers allocates a flat **40 B per call at every length** β€” a constant, + with nothing that scales with the data. This is the number to design around if you recompute in a + tight loop. +* `TAMath` allocates `8 Γ— Length` plus roughly 100–160 B per call, which is exactly one output + `double[]` plus a record header. At `Length = 100000` that is ~800 KB per call β€” a Large Object Heap + allocation, which is why the `Gen2` column is non-zero on those rows and zero everywhere else. +* On a per-call basis `TAMath` costs 4% to 48% more time here. At 100 000 bars the gap is widest, + because that is where the 800 KB allocation and the LOH collection dominate. + +**Do not say:** + +* These are `--job Short` numbers: 3 warmup and 3 target iterations. They are genuine measurements, but + the confidence intervals are wide β€” look at RSI at 100 000, where `Error` is 208 Β΅s on a 639 Β΅s mean. + Do not quote the 1.04x / 1.08x ratios as findings; they are inside the noise. The 1.48x at + `Sma`/100 000 sits well outside it, and the allocation columns are exact at every row. +* They say nothing about any indicator that was not run. Do not interpolate Ema or Macd from Sma. +* They say nothing about your machine. An M1 Max with Server GC on is not a container with one vCPU. + +### What has *not* been measured + +Stated plainly so nobody mistakes silence for a result: + +* **Managed versus native TA-Lib C.** The C library is not installed on the machine used for this page, + so `NativeComparisonBenchmarks` and its equivalence assertions have been compiled and reasoned about + but never executed. **There is no managed-versus-native number anywhere in this repository, and none + should be invented.** Install the library, run + `-- --anyCategories NativeComparison --job Short`, and publish what you get. +* **Candlestick `double` / `float` / `decimal`.** Verified to run end to end via `--selfcheck`, not timed + with a real job. +* **The precision suite.** Same: verified, not timed. +* **A full `--job Default` run of anything.** Every number above is `--job Short`. + +If you produce a proper `--job Default` run, the GitHub-markdown exporter output is already in the right +shape to paste in here, under a heading naming your hardware, OS, .NET version and command. + +--- + +## ⚠️ Methodology caveats + +Read these before quoting anything from this suite. + +1. **`--job Dry` numbers are not measurements.** One cold iteration, dominated by JIT. Use Dry to prove a + benchmark runs; never to compare two of them. The *allocation* column of a Dry run is still valid. +2. **`--job Short` numbers are low-confidence measurements.** Three iterations catch an order of + magnitude, not a 5% regression. Publish `--job Default` for anything that matters. +3. **Compare ratios, not absolutes.** Absolute microseconds are a property of the machine that produced + them; a ratio of two means from the *same table* travels, the `Mean` column does not. Outside + `NativeComparisonBenchmarks` there is no `Ratio` column to read β€” compute the ratio from the two rows of + the same indicator, and never across indicators. +4. **The machine matters more than you think.** Thermal throttling, a browser in the background, a + virtualised CPU, and battery-versus-mains all move these numbers by tens of percent. Re-run a + surprising result before believing it. +5. **Server GC is on** in this project's configuration, and the banner prints it. Allocation-heavy + benchmarks look better under Server GC than they will under Workstation GC in a small container. +6. **Synthetic data is not market data.** It is smooth, gap-free, always well-formed, and never halts. + Indicators with data-dependent branches β€” SAR, the candlestick recognisers β€” may behave differently on + a real series with limit moves and stale prints. +7. **P/Invoke overhead is inside the native numbers.** The native comparison runs in the same process, so + the marshalling cost is charged to the native side. That is realistic for how you would actually call + it, and it is not the same as measuring the C function in isolation. +8. **`--selfcheck` is a gate, not a benchmark.** It never produces a timing and must never be quoted as + one. +9. **First run pays for a build.** Add `--no-build` (after building once) if you are timing the harness + rather than the code. + +--- + +## Related + +* [`benchmarks/TechnicalAnalysis.Benchmarks/README.md`](../../benchmarks/TechnicalAnalysis.Benchmarks/README.md) + β€” the project-local README, with the file-by-file layout. +* [Getting started](getting-started.md) β€” `RetCode`, `BegIdx`/`NBElement`, and the alignment rule the + benchmarks assume you already understand. +* [Indicator catalog](../indicators/README.md) β€” every entry point, its signature and its outputs. diff --git a/docs/guides/fluent-api.md b/docs/guides/fluent-api.md new file mode 100644 index 00000000..688fa2fd --- /dev/null +++ b/docs/guides/fluent-api.md @@ -0,0 +1,1070 @@ +# ✨ The fluent API + +`TAMath` and `TAFunc` are faithful. They give you exactly what TA-Lib gives you: an output array filled +from index `0`, a `BegIdx` telling you which bar element `0` belongs to, and an `NBElement` telling you +how many elements are real. Everything after that is your arithmetic, and +[Β§3 of the getting started guide](getting-started.md#3-retcode-begidx-nbelement--the-part-that-actually-matters) +exists because that arithmetic is easy to get wrong in a way that never throws. + +The fluent API is the same indicators with the arithmetic already done. You build a `PriceSeries`, you +call an indicator on it, and you get back an `IndicatorSeries` in which **every index is a bar index** and +a bar that has not warmed up yet is `null`. There is no second index space to confuse with the first, +because the type does not expose one. + +```csharp +using TechnicalAnalysis.Functions; + +PriceSeries prices = PriceSeries.FromClose(closes); +IndicatorSeries sma = prices.Sma(30); + +Console.WriteLine(sma.Latest); +``` + +Real output, with `closes` set to `1, 2, 3, … 100`: + +```text +85.5 +``` + +> Every code block below was compiled and executed against the source in this repository. The numbers +> printed in the worked examples are real program output, not illustrations. + +--- + +## πŸ“ Table of contents + + +* [✨ The fluent API](#-the-fluent-api) + * [πŸ“ Table of contents](#-table-of-contents) + * [🧨 The problem it solves](#-the-problem-it-solves) + * [Two index spaces](#two-index-spaces) + * [The measured proof](#the-measured-proof) + * [One type owns the conversion](#one-type-owns-the-conversion) + * [🧱 `PriceSeries` β€” the input](#-priceseries--the-input) + * [The four factories](#the-four-factories) + * [Missing components are never fabricated](#missing-components-are-never-fabricated) + * [Non-finite prices are refused at the boundary](#non-finite-prices-are-refused-at-the-boundary) + * [There is no `startIdx` or `endIdx`](#there-is-no-startidx-or-endidx) + * [πŸ“ `IndicatorSeries` β€” the output](#-indicatorseries--the-output) + * [The full surface](#the-full-surface) + * [Warm-up: absence is `null`](#warm-up-absence-is-null) + * [Reading a whole series](#reading-a-whole-series) + * [Projections onto the bar axis](#projections-onto-the-bar-axis) + * [Crossings](#crossings) + * [`AsOf` β€” windowing without look-ahead](#asof--windowing-without-look-ahead) + * [πŸ“Š The indicators](#-the-indicators) + * [Overlap studies](#overlap-studies) + * [Momentum](#momentum) + * [Volatility](#volatility) + * [Volume](#volume) + * [Defaults are TA-Lib's, not the conventional ones](#defaults-are-ta-libs-not-the-conventional-ones) + * [The measured warm-up ladder](#the-measured-warm-up-ladder) + * [πŸšͺ `Align` β€” the escape hatch to the other 89 indicators](#-align--the-escape-hatch-to-the-other-89-indicators) + * [⚠️ Error handling](#-error-handling) + * [What throws](#what-throws) + * [What quietly produces nothing](#what-quietly-produces-nothing) + * [`RetCode` is not warmth](#retcode-is-not-warmth) + * [πŸ™… What the fluent API deliberately does not do](#-what-the-fluent-api-deliberately-does-not-do) + * [πŸ” When to prefer `TAMath`](#-when-to-prefer-tamath) + * [🧩 Composing with the samples](#-composing-with-the-samples) + * [Backtesting](#backtesting) + * [Real-time streaming](#real-time-streaming) + * [Related](#related) + + +--- + +## 🧨 The problem it solves + +### Two index spaces + +There are two, and they are not the same. + +An **array index** is a subscript into the raw `double[]` a `TAMath` call returned. TA-Lib fills that +array from index `0` and writes exactly `NBElement` elements, so the last valid array index is +`NBElement - 1`. + +A **bar index** is a position in your price series, with domain `[0, BarCount)`. `BegIdx` is a *bar* +index: output element `k` describes bar `BegIdx + k`, so the last valid bar index is +`BegIdx + NBElement - 1`. + +```text +NBElement - 1 <- last valid ARRAY index +BegIdx + NBElement - 1 <- last valid BAR index +``` + +Those are two different numbers whenever `BegIdx > 0`, which is every indicator with a lookback, which is +almost all of them. + +### The measured proof + +Subscripting the output array with the *bar* index is the classic bug. Here it is, on a 30-period SMA over +closes `1, 2, 3, … 100`, where the correct answer is the mean of `71 … 100`, which is `85.5`: + +```csharp +using TechnicalAnalysis.Functions; + +double[] closes = [.. Enumerable.Range(1, 100).Select(i => (double)i)]; +SmaResult sma = TAMath.Sma(0, closes.Length - 1, closes, timePeriod: 30); + +Console.WriteLine($"BegIdx={sma.BegIdx} NBElement={sma.NBElement} Real.Length={sma.Real.Length}"); +Console.WriteLine($"Real[NBElement - 1] = {sma.Real[sma.NBElement - 1]}"); +Console.WriteLine($"Real[BegIdx + NBElement - 1] = {sma.Real[sma.BegIdx + sma.NBElement - 1]}"); +``` + +Real output: + +```text +BegIdx=29 NBElement=71 Real.Length=100 +Real[NBElement - 1] = 85.5 +Real[BegIdx + NBElement - 1] = 0 +``` + +The wrong expression does not throw. `BegIdx + NBElement - 1` is `99`, the array holds `100` elements, and +element `99` is one of the untouched zeros past `NBElement`. It returns `0.0` β€” a number that looks like a +number. An RSI read that way is pinned at `0` and reports "oversold" on every bar of every input, forever, +with `RetCode.Success`. + +The same question through the fluent API: + +```csharp +using TechnicalAnalysis.Functions; + +double[] closes = [.. Enumerable.Range(1, 100).Select(i => (double)i)]; +IndicatorSeries sma = PriceSeries.FromClose(closes).Sma(30); + +Console.WriteLine($"BarCount={sma.BarCount} WarmCount={sma.WarmCount} FirstBar={sma.FirstBar} LastBar={sma.LastBar}"); +Console.WriteLine($"Latest = {sma.Latest}"); +Console.WriteLine($"sma[99] = {sma[99]}"); +Console.WriteLine($"sma[29] = {sma[29]}"); +Console.WriteLine($"sma[28] = {sma[28]}"); +``` + +Real output: + +```text +BarCount=100 WarmCount=71 FirstBar=29 LastBar=99 +Latest = 85.5 +sma[99] = 85.5 +sma[29] = 15.5 +sma[28] = +``` + +`sma[28]` prints as empty because it is `null` β€” bar 28 is inside the series but before the indicator +warmed up. `sma[29]` is `15.5`, the mean of `1 … 30`, which you can check by hand. + +### One type owns the conversion + +The whole point is that the conversion between the two index spaces happens in exactly **one** private +expression, inside `IndicatorSeries`, covered by a regression suite β€” not once per property, not once per +call site. Every fluent indicator, every projection, every crossing and the escape hatch all route through +it. + +That is why the surface has no member that takes or returns a raw output-array index, with one deliberate, +explicitly-named exception (`WarmValues`). The historic wrong expression is not merely discouraged; on a +`WarmValues` span it is out of range: + +```csharp +double[] close = [100.0, 102.0, 101.0, 105.0, 107.0, 106.0, 110.0, 111.0]; +IndicatorSeries sma = PriceSeries.FromClose(close).Sma(3); + +ReadOnlySpan warm = sma.WarmValues; +Console.WriteLine($"warm.Length={warm.Length} warm[0]={warm[0]} warm[^1]={warm[^1]}"); +_ = warm[sma.FirstBar!.Value + sma.WarmCount - 1]; // throws +``` + +Real output: + +```text +warm.Length=6 warm[0]=101 warm[^1]=109 +Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. +``` + +The span is sliced to `WarmCount`, so the zero padding is unreachable. A loud failure replaces a silent +`0.0`. + +--- + +## 🧱 `PriceSeries` β€” the input + +`PriceSeries` is an immutable `readonly struct` in `TechnicalAnalysis.Functions`. Its factories **copy**, +so immutability is unconditional rather than a promise you have to keep, and both `PriceSeries` and every +`IndicatorSeries` derived from it are safe for unrestricted concurrent use. + +### The four factories + +All four take `ReadOnlySpan`, so an oversized scratch buffer costs only its valid region: +`PriceSeries.FromHlc(high.AsSpan(0, n), low.AsSpan(0, n), close.AsSpan(0, n))`. + +| Factory | Signature | Unlocks | +| --- | --- | --- | +| `FromClose` | `(ReadOnlySpan close)` | `Sma`, `Ema`, `BollingerBands`, `Rsi`, `Macd` | +| `FromHlc` | `(high, low, close)` | …plus `Atr`, `Adx`, `Stoch` | +| `FromOhlc` | `(open, high, low, close)` | …plus the `Open` span | +| `FromOhlcv` | `(open, high, low, close, volume)` | …plus `Obv` | + +`close` defines `BarCount`; every other component must have the same length or the factory throws +`ArgumentException`. + +```csharp +using TechnicalAnalysis.Functions; + +PriceSeries c = PriceSeries.FromClose(close); +PriceSeries hlc = PriceSeries.FromHlc(high, low, close); +PriceSeries ohlcv = PriceSeries.FromOhlcv(open, high, low, close, volume); + +Console.WriteLine($"FromClose : BarCount={c.BarCount} HasOpen={c.HasOpen} HasHighLow={c.HasHighLow} HasVolume={c.HasVolume}"); +Console.WriteLine($"FromHlc : BarCount={hlc.BarCount} HasOpen={hlc.HasOpen} HasHighLow={hlc.HasHighLow} HasVolume={hlc.HasVolume}"); +Console.WriteLine($"FromOhlcv : BarCount={ohlcv.BarCount} HasOpen={ohlcv.HasOpen} HasHighLow={ohlcv.HasHighLow} HasVolume={ohlcv.HasVolume}"); +``` + +Real output on a 60-bar series: + +```text +FromClose : BarCount=60 HasOpen=False HasHighLow=False HasVolume=False +FromHlc : BarCount=60 HasOpen=False HasHighLow=True HasVolume=False +FromOhlcv : BarCount=60 HasOpen=True HasHighLow=True HasVolume=True +``` + +The rest of the surface is `BarCount`, `IsEmpty`, the three `Has*` flags, the five component spans +(`Open`, `High`, `Low`, `Close`, `Volume`, each `ReadOnlySpan` of exactly `BarCount` elements), +`AsOf`, the two `Align` overloads, and `PriceSeries.Empty` β€” which is `default(PriceSeries)`, a zero-bar +series carrying no components at all. + +### Missing components are never fabricated + +A close-only series does not pretend that high and low equal the close. Asking anyway throws +`InvalidOperationException` and names the factory that would have supplied what is missing: + +```csharp +PriceSeries closeOnly = PriceSeries.FromClose(close); +closeOnly.Atr(14); +``` + +Real output: + +```text +System.InvalidOperationException: Atr needs the high and low of each bar, which this price series does +not carry. Build it with PriceSeries.FromHlc, FromOhlc or FromOhlcv; the closing price alone is never +substituted for a bar's range. +``` + +This matters more than it looks. Substituting the close for high and low would turn a true range into the +absolute change in close and an ATR into something that is *nearly* right β€” the worst possible failure +mode, because it charts plausibly. + +### Non-finite prices are refused at the boundary + +One `NaN` close β€” a gapped tick, a provider sentinel, a bad CSV parse β€” does not spoil only the bars it +touches. TA-Lib's simple moving average carries a running sum, and `NaN - finite` is `NaN` for ever after; +the EMA, ATR and RSI recursions behave the same way. So the factories refuse: + +```csharp +PriceSeries.FromClose([100.0, 101.0, double.NaN, 103.0]); +``` + +Real output: + +```text +System.ArgumentException: 'close' holds NaN at bar 2. A price series must be finite throughout: a single +non-finite bar propagates through every running sum and every recursion in TA-Lib and silently corrupts +the whole indicator. Clean or drop the bar before building the series. (Parameter 'close') +``` + +The exception names the component and the first offending bar. The factories already walk the data to copy +it, so the check is free. + +### There is no `startIdx` or `endIdx` + +There is nowhere on this surface to pass them. Indicators always analyse the whole series, which has three +consequences: + +* `FirstBar` is always the indicator's true lookback, never an artefact of where you asked output to start. +* You cannot trigger the unguarded end-index buffer overrun a raw call permits β€” `TAMath.Sma(0, 100, eightBars, 3)` + throws `IndexOutOfRangeException`, and there is no way to write that here. +* Windowing is [`AsOf`](#asof--windowing-without-look-ahead), which keeps bar indices absolute instead of + rebasing them. + +--- + +## πŸ“ `IndicatorSeries` β€” the output + +### The full surface + +`IndicatorSeries` is an immutable `readonly struct`. **Every `int` on it that names a position is a bar +index.** + +| Member | Type | Meaning | +| --- | --- | --- | +| `this[int bar]` | `double?` | The value at that **bar**, or `null` if the bar has no value | +| `Latest` | `double?` | The value at `LastBar`, or `null` if no bar has one | +| `FirstBar` | `int?` | Bar index of the first value β€” TA-Lib's `BegIdx`, reinterpreted | +| `LastBar` | `int?` | Bar index of the last value, i.e. `FirstBar + WarmCount - 1` | +| `BarCount` | `int` | Bars in the source series. **This, not `WarmCount`, bounds a loop over the indexer** | +| `WarmCount` | `int` | How many bars carry a value β€” TA-Lib's `NBElement`. A count in array space | +| `HasValues` | `bool` | `WarmCount > 0`. The only warmth test | +| `RetCode` | `RetCode` | The raw status. Says nothing about whether there are values | +| `WarmValues` | `ReadOnlySpan` | The one array-indexed view, sliced to `WarmCount` | +| `IsWarmAt(int bar)` | `bool` | Equivalent to `this[bar] is not null`, without the nullable | +| `AsOf(int bar)` | `IndicatorSeries` | The same series truncated to end at `bar` | +| `CrossedAbove(double level, int bar)` | `bool` | Crossed up through a fixed level at `bar` | +| `CrossedBelow(double level, int bar)` | `bool` | Crossed down through a fixed level at `bar` | +| `CrossedAbove(IndicatorSeries other, int bar)` | `bool` | Crossed up through another series at `bar` | +| `CrossedBelow(IndicatorSeries other, int bar)` | `bool` | Crossed down through another series at `bar` | +| `ToBarAlignedArray()` | `double[]` | `BarCount` elements, bar-indexed, `NaN` where there is no value | +| `ToBarAlignedNullableArray()` | `double?[]` | `BarCount` elements, bar-indexed, `null` where there is none | +| `WarmValuesToArray()` | `double[]` | `WarmCount` elements, array-indexed like `WarmValues` | +| `CopyBarAligned(Span)` | `void` | Writes the bar-aligned projection into a buffer you own | +| `GetEnumerator()` | `Enumerator` | Allocation-free `foreach` over `(int Bar, double Value)` pairs | +| `IndicatorSeries.Empty(int barCount)` | `static` | A series of `barCount` bars in which nothing has a value | +| `IndicatorSeries.Create(…)` | `static` | The only public door for raw TA-Lib metadata; see [`Align`](#-align--the-escape-hatch-to-the-other-89-indicators) | + +Two names are worth dwelling on. `WarmCount` is deliberately not called `Count`, because on a type with an +indexer `Count` reads as "the number of valid indices", and it is not: the indexer's domain is +`[0, BarCount)`. `for (int i = 0; i < s.WarmCount; i++) s[i]` would read the wrong bars and silently drop +the most recent ones. And `WarmValues` is not called `Values`, because it is the only array-indexed thing +on the type and its name has to say so. + +### Warm-up: absence is `null` + +A bar inside the series but before the indicator warmed up has no value, and that is reported as `null` β€” +never `0.0`, never `NaN`, never an exception. + +```csharp +using TechnicalAnalysis.Functions; + +double[] close = [100.0, 102.0, 101.0, 105.0, 107.0, 106.0, 110.0, 111.0]; +IndicatorSeries sma = PriceSeries.FromClose(close).Sma(3); + +Console.WriteLine($"BarCount={sma.BarCount} WarmCount={sma.WarmCount} FirstBar={sma.FirstBar} LastBar={sma.LastBar}"); + +for (int bar = 0; bar < sma.BarCount; bar++) +{ + Console.WriteLine($"bar {bar}: close={close[bar]} sma={(sma[bar] is { } v ? v.ToString("0.####") : "null")}"); +} +``` + +Real output: + +```text +BarCount=8 WarmCount=6 FirstBar=2 LastBar=7 +bar 0: close=100 sma=null +bar 1: close=102 sma=null +bar 2: close=101 sma=101 +bar 3: close=105 sma=102.6667 +bar 4: close=107 sma=104.3333 +bar 5: close=106 sma=106 +bar 6: close=110 sma=107.6667 +bar 7: close=111 sma=109 +``` + +That is the same eight-bar series and the same numbers as +[Β§3.2 of the getting started guide](getting-started.md#32-a-fully-worked-example), where they were obtained +by projecting `Real[k]` onto bar `BegIdx + k` by hand. + +The idiomatic read is a pattern match, which gets you a non-nullable `double` and skips the warm-up in one +step: + +```csharp +if (sma[bar] is { } value && value > threshold) +{ + // ... +} +``` + +Asking about a bar **outside** `[0, BarCount)` is a different thing entirely and throws +`ArgumentOutOfRangeException`. "Bar 5 of a 30-period SMA" is a legitimate question whose answer is "no +value"; "bar 5000 of a 100-bar series" is not a question. Keeping those two apart is what makes `AsOf` a +guarantee rather than a convention. + +### Reading a whole series + +`foreach` yields only the bars that have a value, as `(Bar, Value)` pairs, and allocates nothing: + +```csharp +foreach ((int bar, double value) in sma) +{ + Console.WriteLine($"bar {bar} -> {value:0.####}"); +} +``` + +Real output: + +```text +bar 2 -> 101 +bar 3 -> 102.6667 +bar 4 -> 104.3333 +bar 5 -> 106 +bar 6 -> 107.6667 +bar 7 -> 109 +``` + +`IEnumerable` is deliberately not implemented β€” `foreach` binds to the enumerator pattern directly, and +implementing the interface would box the enumerator on every loop. Use `WarmValuesToArray()` or +`ToBarAlignedNullableArray()` when you need something LINQ can consume. + +### Projections onto the bar axis + +Three projections, for three different consumers: + +```csharp +double[] padded = sma.ToBarAlignedArray(); +double?[] nullable = sma.ToBarAlignedNullableArray(); +double[] warm = sma.WarmValuesToArray(); +``` + +Real output: + +```text +ToBarAlignedArray() : NaN, NaN, 101, 102.6667, 104.3333, 106, 107.6667, 109 +ToBarAlignedNullableArray() : null, null, 101, 102.6667, 104.3333, 106, 107.6667, 109 +WarmValuesToArray() : 101, 102.6667, 104.3333, 106, 107.6667, 109 +lengths: 8, 8, 6 +``` + +`ToBarAlignedArray` pads with `NaN` and the padding is **not** configurable, because padding with `0.0` +reinstates exactly the corruption this whole type exists to prevent. It is the one place on the surface +where absence is a sentinel rather than `null`, so it cannot distinguish "no value here" from "a value +that is not finite"; `ToBarAlignedNullableArray` is the projection that keeps the promise. And because +every indicator has a warm-up, `ToBarAlignedArray().Average()` is `NaN` for a typical series β€” a chart fed +the raw array has to be told how to skip them. + +`CopyBarAligned(Span)` writes the same projection into a buffer you already own, which is what a +per-frame rendering loop wants: + +```csharp +double[] buffer = new double[prices.BarCount]; +prices.Sma(3).CopyBarAligned(buffer); +``` + +Real output: + +```text +NaN, NaN, 101, 102.6667, 104.3333, 106, 107.6667, 109 +``` + +It throws `ArgumentException` if the destination is shorter than `BarCount`; anything beyond `BarCount` is +left untouched. + +### Crossings + +A crossing is a **transition between two bars**, not a state. A series already above a level does not keep +reporting a crossing, and a crossing at bar `0` is always `false` because there is no previous bar. If any +of the values involved is missing, the answer is `false` β€” which is what makes a fast/slow pair with +different warm-ups work with no reasoning at the call site. + +```csharp +using TechnicalAnalysis.Functions; + +PriceSeries prices = PriceSeries.FromClose(close); +IndicatorSeries fast = prices.Sma(5); +IndicatorSeries slow = prices.Sma(20); + +for (int bar = 0; bar < prices.BarCount; bar++) +{ + if (fast.CrossedAbove(slow, bar)) + { + Console.WriteLine($"bar {bar}: golden cross (fast={fast[bar]:0.####} slow={slow[bar]:0.####})"); + } + else if (fast.CrossedBelow(slow, bar)) + { + Console.WriteLine($"bar {bar}: death cross (fast={fast[bar]:0.####} slow={slow[bar]:0.####})"); + } +} +``` + +Real output on the 60-bar demo series: + +```text +bar 35: golden cross (fast=102.7612 slow=102.0938) +bar 57: death cross (fast=115.7357 slow=116.6491) +``` + +The fixed-level overloads read the same way. Note that they test a *transition*, so a persistently +overbought RSI fires once, not on every bar: + +```csharp +IndicatorSeries rsi = prices.Rsi(14); + +for (int bar = 0; bar < prices.BarCount; bar++) +{ + if (rsi.CrossedBelow(70.0, bar)) + { + Console.WriteLine($"bar {bar}: RSI fell back through 70 ({rsi[bar]:0.####})"); + } +} +``` + +Real output: + +```text +bar 18: RSI fell back through 70 (64.9599) +bar 54: RSI fell back through 70 (66.6238) +``` + +Crossing two series computed over price series of different lengths is a caller bug and throws +`ArgumentException`, because a bar index would not mean the same thing in both. + +### `AsOf` β€” windowing without look-ahead + +`AsOf(bar)` returns the same series truncated so that it ends at `bar`. Bar indices are **not** rebased: +they stay absolute positions in the original price series. Asking the narrowed series about a later bar +throws, because that bar is not part of the value at all β€” the future is not merely guarded, it is absent. + +Both `PriceSeries` and `IndicatorSeries` have it, and because every shipped indicator is causal the two +routes agree exactly: + +```csharp +IndicatorSeries whole = prices.Sma(20); + +double? narrowed = whole.AsOf(40).Latest; +double? recomputed = prices.AsOf(40).Sma(20).Latest; + +Console.WriteLine($"whole.AsOf(40).Latest = {narrowed}"); +Console.WriteLine($"prices.AsOf(40).Sma(20).Latest = {recomputed}"); +Console.WriteLine($"equal = {narrowed == recomputed}"); +Console.WriteLine($"whole.AsOf(40).BarCount={whole.AsOf(40).BarCount} FirstBar={whole.AsOf(40).FirstBar}"); +whole.AsOf(40)[41]; // throws +``` + +Real output: + +```text +whole.AsOf(40).Latest = 103.16686499999994 +prices.AsOf(40).Sma(20).Latest = 103.16686499999994 +equal = True +whole.AsOf(40).BarCount=41 FirstBar=19 +System.ArgumentOutOfRangeException: The bar index must lie in [0, 41). A bar inside that range that has +no value is reported as null rather than as an exception. (Parameter 'bar') +Actual value was 41. +``` + +Prefer the first route. `IndicatorSeries.AsOf` is allocation-free and `O(1)`; recomputing the indicator +inside a per-bar loop is `O(nΒ²)` for the identical answer. `MacdSeries`, `BollingerBandsSeries` and +`StochSeries` each expose an `AsOf` that narrows all of their components at once. + +--- + +## πŸ“Š The indicators + +Nine indicators ship as extension methods on `PriceSeries`, in the same +`TechnicalAnalysis.Functions` namespace. The declaring classes (`OverlapStudyIndicators`, +`MomentumIndicators`, `VolatilityIndicators`, `VolumeIndicators`) are TA-Lib's own function groups and +never appear at a call site. + +The examples below all run against a deterministic 60-bar series, so you can reproduce every number: + +```csharp +static (double[] Open, double[] High, double[] Low, double[] Close, double[] Volume) Demo(int bars = 60) +{ + double[] open = new double[bars]; + double[] high = new double[bars]; + double[] low = new double[bars]; + double[] close = new double[bars]; + double[] volume = new double[bars]; + + for (int i = 0; i < bars; i++) + { + double mid = 100.0 + (i * 0.25) + (8.0 * Math.Sin(i / 6.0)); + close[i] = Math.Round(mid, 4); + high[i] = Math.Round(mid + 1.5, 4); + low[i] = Math.Round(mid - 1.5, 4); + open[i] = i == 0 ? close[0] : close[i - 1]; + volume[i] = 1000 + (i * 10); + } + + return (open, high, low, close, volume); +} +``` + +It is referenced below as `var (open, high, low, close, volume) = Demo();`. + +### Overlap studies + +```csharp +public static IndicatorSeries Sma(this PriceSeries prices, int timePeriod = 30); +public static IndicatorSeries Ema(this PriceSeries prices, int timePeriod = 30); +public static BollingerBandsSeries BollingerBands( + this PriceSeries prices, + int timePeriod = 5, + double nbDevUp = 2.0, + double nbDevDn = 2.0, + MAType maType = MAType.Sma); +``` + +All three read closes only, so `FromClose` is enough. `BollingerBandsSeries` is a +`readonly record struct (IndicatorSeries Upper, IndicatorSeries Middle, IndicatorSeries Lower)` with an +`AsOf`. + +```csharp +using TechnicalAnalysis.Functions; + +var (open, high, low, close, volume) = Demo(); +PriceSeries prices = PriceSeries.FromClose(close); + +IndicatorSeries sma = prices.Sma(20); +IndicatorSeries ema = prices.Ema(20); +BollingerBandsSeries bb = prices.BollingerBands(timePeriod: 20, nbDevUp: 2.0, nbDevDn: 2.0); + +Console.WriteLine($"Sma(20) FirstBar={sma.FirstBar} Latest={sma.Latest:0.####}"); +Console.WriteLine($"Ema(20) FirstBar={ema.FirstBar} Latest={ema.Latest:0.####}"); +Console.WriteLine($"BB FirstBar={bb.Middle.FirstBar} Upper={bb.Upper.Latest:0.####} Middle={bb.Middle.Latest:0.####} Lower={bb.Lower.Latest:0.####}"); +Console.WriteLine($"BB default period FirstBar={prices.BollingerBands().Middle.FirstBar}"); +``` + +Real output: + +```text +Sma(20) FirstBar=19 Latest=116.7883 +Ema(20) FirstBar=19 Latest=114.5205 +BB FirstBar=19 Upper=122.0419 Middle=116.7883 Lower=111.5346 +BB default period FirstBar=4 +``` + +That last line is the point of [the defaults section below](#defaults-are-ta-libs-not-the-conventional-ones): +`prices.BollingerBands()` is a **5**-period band, not a 20-period one. + +### Momentum + +```csharp +public static IndicatorSeries Rsi(this PriceSeries prices, int timePeriod = 14); +public static IndicatorSeries Adx(this PriceSeries prices, int timePeriod = 14); +public static MacdSeries Macd( + this PriceSeries prices, + int fastPeriod = 12, + int slowPeriod = 26, + int signalPeriod = 9); +public static StochSeries Stoch( + this PriceSeries prices, + int fastKPeriod = 5, + int slowKPeriod = 3, + MAType slowKMAType = MAType.Sma, + int slowDPeriod = 3, + MAType slowDMAType = MAType.Sma); +``` + +`Rsi` and `Macd` need closes only. `Adx` and `Stoch` need high and low as well, because directional +movement and "where the close sits in the recent range" are both properties of a bar's range. + +`MacdSeries` is `(Line, Signal, Histogram)`; `StochSeries` is `(SlowK, SlowD)` β€” the same component names +as `StochResult`, so moving between the two layers costs nothing. Each component is a full +`IndicatorSeries`, which is why a signal-line crossing needs no dedicated member: +`macd.Line.CrossedAbove(macd.Signal, bar)` already says it. + +```csharp +PriceSeries prices = PriceSeries.FromHlc(high, low, close); + +IndicatorSeries rsi = prices.Rsi(14); +IndicatorSeries adx = prices.Adx(14); +MacdSeries macd = prices.Macd(); +StochSeries stoch = prices.Stoch(); + +Console.WriteLine($"Rsi(14) FirstBar={rsi.FirstBar} Latest={rsi.Latest:0.####}"); +Console.WriteLine($"Adx(14) FirstBar={adx.FirstBar} Latest={adx.Latest:0.####}"); +Console.WriteLine($"Macd Line.FirstBar={macd.Line.FirstBar} Signal.FirstBar={macd.Signal.FirstBar} Hist.FirstBar={macd.Histogram.FirstBar}"); +Console.WriteLine($"Macd Line={macd.Line.Latest:0.####} Signal={macd.Signal.Latest:0.####} Histogram={macd.Histogram.Latest:0.####}"); +Console.WriteLine($"Stoch SlowK.FirstBar={stoch.SlowK.FirstBar} SlowK={stoch.SlowK.Latest:0.####} SlowD={stoch.SlowD.Latest:0.####}"); +``` + +Real output: + +```text +Rsi(14) FirstBar=14 Latest=41.9323 +Adx(14) FirstBar=27 Latest=40.6353 +Macd Line.FirstBar=33 Signal.FirstBar=33 Hist.FirstBar=33 +Macd Line=1.1881 Signal=2.4023 Histogram=-1.2142 +Stoch SlowK.FirstBar=8 SlowK=20.9371 SlowD=21.56 +``` + +All three MACD components share a `FirstBar`, as do the three Bollinger bands and the two stochastic +lines: a multi-output indicator emits nothing until all of its outputs exist. + +`Rsi` is bounded to `0 … 100`. A strictly rising series pins it at `100`, and a perfectly flat series +reads `0`, matching the reference C implementation. Because a series with no values answers `null` rather +than `0`, an RSI that has not warmed up can never be mistaken for an oversold one. + +### Volatility + +```csharp +public static IndicatorSeries Atr(this PriceSeries prices, int timePeriod = 14); +``` + +Needs high and low. Its `FirstBar` is `timePeriod`, not `timePeriod - 1`, because the first true range +needs a previous close. + +```csharp +IndicatorSeries atr = PriceSeries.FromHlc(high, low, close).Atr(14); +Console.WriteLine($"Atr(14) FirstBar={atr.FirstBar} Latest={atr.Latest:0.####}"); + +// A series whose true range is exactly 4.0 on every bar. +double[] h = [.. Enumerable.Repeat(102.0, 40)]; +double[] l = [.. Enumerable.Repeat(98.0, 40)]; +double[] c = [.. Enumerable.Repeat(100.0, 40)]; +IndicatorSeries flat = PriceSeries.FromHlc(h, l, c).Atr(14); +Console.WriteLine($"Atr over a constant true range of 4.0: FirstBar={flat.FirstBar} Latest={flat.Latest}"); +``` + +Real output: + +```text +Atr(14) FirstBar=14 Latest=3.0038 +Atr over a constant true range of 4.0: FirstBar=14 Latest=4 +``` + +### Volume + +```csharp +public static IndicatorSeries Obv(this PriceSeries prices); +``` + +Needs volumes, which only `FromOhlcv` supplies. It has no lookback, so it is warm from bar `0`: + +```csharp +IndicatorSeries obv = PriceSeries.FromOhlcv(close, high, low, close, volume).Obv(); +Console.WriteLine($"Obv FirstBar={obv.FirstBar} WarmCount={obv.WarmCount} Latest={obv.Latest}"); +``` + +Real output: + +```text +Obv FirstBar=0 WarmCount=60 Latest=5580 +``` + +The absolute level of OBV carries no meaning β€” it depends on where your series happens to start β€” so only +its direction and its divergence from price are read. + +### Defaults are TA-Lib's, not the conventional ones + +Every default in the table below is copied verbatim from the corresponding raw entry point. The fluent +layer never invents a number, so a fluent call and a raw call with the same arguments cannot disagree. + +| Indicator | Defaults | Note | +| --- | --- | --- | +| `Sma` / `Ema` | `timePeriod: 30` | | +| `BollingerBands` | `timePeriod: 5`, `nbDevUp: 2.0`, `nbDevDn: 2.0`, `maType: MAType.Sma` | **20 is the conventional choice β€” pass it explicitly** | +| `Rsi` | `timePeriod: 14` | | +| `Adx` | `timePeriod: 14` | | +| `Macd` | `fastPeriod: 12`, `slowPeriod: 26`, `signalPeriod: 9` | | +| `Stoch` | `fastKPeriod: 5`, `slowKPeriod: 3`, `slowKMAType: Sma`, `slowDPeriod: 3`, `slowDMAType: Sma` | | +| `Atr` | `timePeriod: 14` | | +| `Obv` | *none* | | + +`nbDevUp` and `nbDevDn` are not validated β€” any finite deviation is meaningful. Every `int` period is, +and the rule is uniform: **2 to 100000**, on every indicator. That is deliberately stricter than parts of +the raw layer, where a handful of entry points accept `1`; one rule that cannot be got wrong is worth more +than per-indicator fidelity to an inconsistency, and it is what stops a MACD signal period of `1` from +failing inside an internal array copy. + +### The measured warm-up ladder + +`FirstBar` for the default parameters, measured on the 60-bar demo series: + +| Call | `FirstBar` | Why | +| --- | ---: | --- | +| `Obv()` | 0 | A running total needs no history | +| `BollingerBands()` | 4 | `timePeriod - 1`, with TA-Lib's default period of 5 | +| `Stoch()` | 8 | `fastK - 1 + slowK - 1 + slowD - 1` | +| `Atr(14)` | 14 | The first true range needs a previous close | +| `Rsi(14)` | 14 | | +| `Sma(20)` / `Ema(20)` | 19 | `timePeriod - 1` | +| `Adx(14)` | 27 | A smoothed average of a smoothed average β€” the longest on this surface | +| `Macd()` | 33 | Slow EMA, then the signal EMA on top of it | + +Raising `TACore.Globals.UnstablePeriod[…]` pushes these out further; see +[Β§7.1 of the getting started guide](getting-started.md#71-unstable-periods). Read `FirstBar` from the +result rather than hard-coding any of these. + +--- + +## πŸšͺ `Align` β€” the escape hatch to the other 89 indicators + +Nine indicators have a fluent wrapper. The catalog has [98](../indicators/README.md). `PriceSeries.Align` +reaches the rest through the *same* primitive the shipped wrappers use, so an indicator aligned this way is +aligned exactly as carefully as one that ships: + +```csharp +public IndicatorSeries Align(SingleOutputResult result); +public IndicatorSeries Align(TResult result, Func output) + where TResult : IndicatorResult; +``` + +The raw result must have been computed over the same prices with `startIdx: 0` and +`endIdx: BarCount - 1`, otherwise its metadata does not describe these bars. The bar count is supplied by +the series itself, so it can never be mismatched. + +```csharp +using TechnicalAnalysis.Functions; + +var (_, high, low, close, _) = Demo(); +PriceSeries prices = PriceSeries.FromHlc(high, low, close); +int last = prices.BarCount - 1; + +IndicatorSeries cci = prices.Align(TAMath.Cci(0, last, high, low, close, timePeriod: 20)); +IndicatorSeries aroonUp = prices.Align(TAMath.Aroon(0, last, high, low, timePeriod: 14), r => r.AroonUp); + +Console.WriteLine($"Cci(20) FirstBar={cci.FirstBar} Latest={cci.Latest:0.####}"); +Console.WriteLine($"AroonUp FirstBar={aroonUp.FirstBar} Latest={aroonUp.Latest:0.####}"); +``` + +Real output: + +```text +Cci(20) FirstBar=19 Latest=-152.5456 +AroonUp FirstBar=14 Latest=21.4286 +``` + +Three things about that call worth knowing: + +* **The multi-output overload takes a selector, not an array.** Passing + `Align(result, r => r.AroonUp)` guarantees the metadata and the values come from the same result. + Handing it a bare `double[]` would have opened a brand new way to pair one result's `BegIdx` with + another result's numbers. +* **The values are copied**, so `result.Real` stays yours and may be post-processed in place afterwards. + The copy is `O(NBElement)` against an `O(n)` computation. +* **You still need your own arrays.** `prices.Close` is a `ReadOnlySpan` and `TAMath` wants + `double[]`, so pass the arrays you already had when you built the series. + +If a result's metadata does not fit the series β€” `BegIdx + NBElement` past `BarCount`, or `NBElement` past +its own array β€” `Align` throws `ArgumentException` naming `result`. It does not clamp. Clamping would hand +back a silently shifted series, which is the failure this whole design exists to prevent. + +> **Naming your own extensions.** The shipped indicators are extension methods on `PriceSeries` in the +> `TechnicalAnalysis.Functions` namespace, and that set will grow towards the full TA-Lib surface. A +> user-authored `public static IndicatorSeries Cci(this PriceSeries, int)` becomes ambiguous (`CS0121`) +> the day the library ships its own `Cci`. Give your own extensions names the library will never take β€” a +> prefix, or a receiver type of your own. + +--- + +## ⚠️ Error handling + +The rule is: **a bad call throws, a short series does not.** + +### What throws + +| Situation | Exception | +| --- | --- | +| A period outside `2 … 100000` | `ArgumentOutOfRangeException`, naming the parameter | +| A bar index outside `[0, BarCount)` | `ArgumentOutOfRangeException` | +| A component array whose length differs from `close` | `ArgumentException`, naming the component | +| A non-finite price in any component | `ArgumentException`, naming the component and the first bad bar | +| `Atr` / `Adx` / `Stoch` on a series with no high and low | `InvalidOperationException`, naming the factory to use | +| `Obv` on a series with no volume | `InvalidOperationException` | +| Crossing two series with different `BarCount` | `ArgumentException` | +| A `CopyBarAligned` destination shorter than `BarCount` | `ArgumentException` | +| An `Align` result whose metadata does not fit the series | `ArgumentException`, naming `result` | + +```csharp +prices.Sma(1); +``` + +```text +System.ArgumentOutOfRangeException: The period must lie between 2 and 100000. (Parameter 'timePeriod') +Actual value was 1. +``` + +```csharp +IndicatorSeries sixty = PriceSeries.FromClose(close).Sma(20); // 60 bars +IndicatorSeries thirty = PriceSeries.FromClose(close[..30]).Sma(20); // 30 bars +sixty.CrossedAbove(thirty, 40); +``` + +```text +System.ArgumentException: The other series covers 30 bar(s) but this one covers 60. Two series can only +be crossed when a bar index means the same thing in both. (Parameter 'other') +``` + +Note that periods and price components are validated **before** the series is checked for emptiness, so a +bad period is reported even when there is no data to compute over. + +### What quietly produces nothing + +Not enough data is not an error. It is a series with `BarCount` bars and no values: + +```csharp +IndicatorSeries sma = PriceSeries.FromClose([1.0, 2.0, 3.0]).Sma(30); + +Console.WriteLine($"RetCode={sma.RetCode} HasValues={sma.HasValues} WarmCount={sma.WarmCount} BarCount={sma.BarCount}"); +Console.WriteLine($"FirstBar={sma.FirstBar} Latest={sma.Latest} sma[2]={sma[2]}"); +``` + +Real output: + +```text +RetCode=Success HasValues=False WarmCount=0 BarCount=3 +FirstBar= Latest= sma[2]= +``` + +`FirstBar`, `Latest` and `sma[2]` are all `null`. Nothing is `0.0`; nothing throws. + +`PriceSeries.Empty` (which is `default(PriceSeries)`) is the same idea at zero bars, with one wrinkle +worth knowing: it carries no high, low or volume, so the indicators that need them throw rather than +returning nothing. A feed that must answer every indicator before it has any bars wants +`PriceSeries.FromOhlcv([], [], [], [], [])`, where the components are present and merely empty: + +```text +PriceSeries.Empty: BarCount=0 IsEmpty=True HasHighLow=False +empty.Sma(20): RetCode=Success BarCount=0 HasValues=False Latest= +empty.Atr(14) -> InvalidOperationException (no high/low component) +FromOhlcv([],[],[],[],[]): BarCount=0 Atr HasValues=False Obv HasValues=False +``` + +### `RetCode` is not warmth + +`IndicatorSeries.RetCode` is carried through from the raw call, and it answers only "were the parameters +acceptable". It never answers "are there values" β€” a period longer than the available data is a *success* +that produces nothing, as the block above shows. + +**`HasValues` is the only warmth test.** Note also that `default(IndicatorSeries)` is a valid empty series +reporting `RetCode.Success`, because `Success == 0`. + +In practice you rarely touch `RetCode` at all: the fluent layer validates every parameter the raw layer +could refuse *before* computing, so a non-`Success` code would indicate a library defect and is raised as +`InvalidOperationException` rather than returned. It is exposed so that a series can still be inspected, +not so that you have to branch on it. + +--- + +## πŸ™… What the fluent API deliberately does not do + +* **No thresholded verdicts.** There is no `IsOverbought`, `IsBullish`, `IsStrongTrend` or `IsSqueeze`. A + wrong number looks wrong; a wrong `bool` looks authoritative and has already destroyed the evidence that + would have shown it was wrong. The conventional levels are conventions anyway β€” 70 and 30 come from a + 1978 book about daily bars β€” so they belong in your source file, under your review. +* **No derived measures.** No `%B`, no bandwidth. They are arithmetic on three numbers you already have, + and every convention for computing them is an opinion. +* **No caching.** `prices.Sma(20)` twice computes twice. Because these are value types with no lazy state, + caching is one field at your composition root when you want it, and there is no shared mutable state and + no locking when you do not. +* **No incremental/streaming update.** There is no "append one bar" path; recompute over the whole series, + which is what the [real-time sample](real-time-streaming.md#when-to-move-to-a-true-incremental-indicator) + does and discusses. +* **No `float` or `decimal` overloads.** The fluent surface is `double` only. `TAMath`'s `float[]` + overloads widen to `double[]` and compute in `double` anyway β€” see + [Β§5 of the getting started guide](getting-started.md#5-choosing-double-vs-float). +* **No candlestick patterns yet.** `TACandle` remains the way to detect those. + +--- + +## πŸ” When to prefer `TAMath` + +The fluent API is the right default for application code. Reach past it when: + +* **You need one of the other 89 indicators** and would rather not write the `Align` call β€” though `Align` + is one line and gives you the alignment guarantee for free, so this is rarely the right reason. +* **You need `float[]` or `decimal[]` inputs.** `TAMath` has `float[]` overloads and `TACandle` is generic + over `IFloatingPoint`. +* **You need a genuine sub-range.** The fluent layer always analyses the whole series; `TAMath` lets you + set `startIdx`/`endIdx` explicitly. +* **You are counting allocations in a hot loop.** `TAFunc` lets you own and reuse the output buffer; + `PriceSeries` factories copy their inputs, and each fluent call allocates one output array. The + [benchmarks guide](benchmarks.md#-measured-results) prices the `TAFunc`-versus-`TAMath` gap. +* **You are porting existing code** that already handles `BegIdx`/`NBElement` correctly and you have no + reason to touch it. + +The two layers are not exclusive and produce identical numbers, because the fluent layer *calls* `TAMath`. +Mixing them in one file is fine. + +--- + +## 🧩 Composing with the samples + +Both runnable samples were written before this API landed, and each solved the alignment problem for +itself. They are worth reading as two independent statements of the same rule β€” and as a measure of how +much the fluent API removes. + +### Backtesting + +[`samples/TechnicalAnalysis.Samples.Backtesting`](../../samples/TechnicalAnalysis.Samples.Backtesting) +carries its own `IndicatorSeries`, `MacdSeries` and `BollingerBandSeries` in +`TechnicalAnalysis.Samples.Backtesting.Engine`. **They are different types with the same names as the +library's**, so a file that uses both needs an alias; the sample's versions add a look-ahead guard the +library's do not have. + +The shapes line up closely, which makes the mapping easy to read: + +| Sample engine | Fluent API | +| --- | --- | +| `TryGetValue(bar, out value)` | `series[bar] is { } value` | +| `HasValueAt(bar)` / `HasCurrent` | `series.IsWarmAt(bar)` / `series.Latest is not null` | +| `this[bar]` (throws while warming up) | `series[bar]` (`null` while warming up) | +| `TryGetPair(bar, out prev, out cur)` | `series.CrossedAbove(other, bar)` and friends | +| the window bound that raises `LookAheadException` | `series.AsOf(bar)`, where later bars are simply absent | + +The one thing the sample has that the library does not is causality *enforcement*: reading a future bar +throws `LookAheadException`. `AsOf` gets there differently β€” instead of guarding an index it hands over a +value that does not contain the future, so `prices.AsOf(bar)` given to a strategy makes look-ahead +unrepresentable rather than detectable. The sample's +[no-look-ahead section](backtesting.md#-the-no-look-ahead-guarantee) is worth reading for why that +distinction matters. + +A per-bar strategy loop in fluent terms β€” compute once outside the loop, narrow or index inside it: + +```csharp +using TechnicalAnalysis.Functions; + +PriceSeries prices = PriceSeries.FromHlc(high, low, close); + +IndicatorSeries fast = prices.Sma(5); +IndicatorSeries slow = prices.Sma(20); +IndicatorSeries atr = prices.Atr(14); + +for (int bar = 0; bar < prices.BarCount; bar++) +{ + if (!fast.IsWarmAt(bar) || !slow.IsWarmAt(bar) || !atr.IsWarmAt(bar)) + { + continue; + } + + if (fast.CrossedAbove(slow, bar)) + { + double stop = close[bar] - (3.0 * atr[bar]!.Value); + Console.WriteLine($"bar {bar}: long at {close[bar]:0.####}, stop {stop:0.####}"); + } +} +``` + +Real output on the 60-bar demo series: + +```text +bar 35: long at 105.2713, stop 96.266 +``` + +Computing the three indicators once and indexing per bar is `O(n)`; calling `prices.AsOf(bar).Sma(5)` +inside the loop would be `O(nΒ²)` for the same answer, because every shipped indicator is causal. + +### Real-time streaming + +[`samples/TechnicalAnalysis.Samples.RealTime`](../../samples/TechnicalAnalysis.Samples.RealTime) recomputes +over a rolling window on every closed bar and publishes one nullable field per indicator, where **null +means "no value for this bar"** β€” the same contract as `IndicatorSeries`, expressed on the wire. Its +`RollingIndicatorEngine.Latest` helper does by hand precisely what `IndicatorSeries.Latest` does: read +array index `NBElement - 1`, and refuse to hand back a value whose bar is not the bar being reported. + +In fluent terms, that helper is: + +```csharp +double? latest = PriceSeries.FromHlc(windowHigh, windowLow, windowClose).Rsi(14).Latest; +``` + +with two differences worth naming. The sample additionally returns `null` for a non-finite value, which the +fluent layer reaches from the other end by rejecting non-finite *inputs* at the factory. And the sample +checks that the newest output really belongs to the newest input bar; `IndicatorSeries` gets that for free, +because `Latest` is `LastBar`'s value and `Create` refuses metadata that does not fit the series at all. + +Its [warm-up and null semantics section](real-time-streaming.md#-warm-up-and-null-semantics) has the +measured warm-up ladder against a live feed, and its +[incremental-versus-recompute discussion](real-time-streaming.md#when-to-move-to-a-true-incremental-indicator) +applies unchanged to the fluent API, which is also a full recompute. + +--- + +## Related + +* [Getting started](getting-started.md) β€” installation, `RetCode`/`BegIdx`/`NBElement` from first + principles, `TACore.Globals`, the low-level `TAFunc` API and the common pitfalls. +* [Indicator catalog](../indicators/README.md) β€” all 159 entry points with signatures, defaults and + outputs, for everything `Align` reaches. +* [Backtesting](backtesting.md) β€” the engine model and the structurally enforced no-look-ahead guarantee. +* [Real-time streaming](real-time-streaming.md) β€” the same alignment rule against a live feed. +* [TradingView integration](tradingview-integration.md) β€” parity caveats when reconciling against a chart. diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md new file mode 100644 index 00000000..22c1081d --- /dev/null +++ b/docs/guides/getting-started.md @@ -0,0 +1,659 @@ +# Getting started with TaLibStandard + +TaLibStandard is a C# 14 / .NET 10 port of TA-Lib. It ships 98 technical indicators and 61 +candlestick patterns as pure, allocation-light static methods with no native dependency. + +This guide covers installation, the one concept you must understand before you trust a single +number the library returns (`BegIdx` / `NBElement`), and the traps that bite everybody once. + +> Every code block below was compiled and executed against the source in this repository. The +> numbers printed in the worked examples are real program output, not illustrations. + +--- + +## 1. Installation + +### The meta-package + +`Atypical.TechnicalAnalysis` is a meta-package: it contains no code of its own, it just references +`Atypical.TechnicalAnalysis.Functions` and `Atypical.TechnicalAnalysis.Candles` (which in turn pull +in `Atypical.TechnicalAnalysis.Common`). + +```bash +dotnet add package Atypical.TechnicalAnalysis +``` + +### Individual packages + +If you only need indicators, or only need candlestick patterns, reference just what you use: + +```bash +dotnet add package Atypical.TechnicalAnalysis.Functions # TAMath / TAFunc - 98 indicators +dotnet add package Atypical.TechnicalAnalysis.Candles # TACandle - 61 patterns +``` + +Both depend on `Atypical.TechnicalAnalysis.Common`, which carries the shared types (`RetCode`, +`MAType`, `IndicatorResult` and friends). You never need to reference it explicitly. + +### Namespaces + +There are four, and they are not all the same shape as the package ids: + +| Type | Namespace | Package | +| --- | --- | --- | +| `TAMath`, `TAFunc`, every `*Result` record | `TechnicalAnalysis.Functions` | `Atypical.TechnicalAnalysis.Functions` | +| `TACandle` | `TechnicalAnalysis.Candles` | `Atypical.TechnicalAnalysis.Candles` | +| `RetCode`, `MAType`, `FuncUnstId`, `IndicatorResult`, `CandleIndicatorResult`, `CandleSetting`, `CandleSettingType`, `RangeType`, `Compatibility`, `GlobalsType`, `ValidationHelper` | `TechnicalAnalysis.Common` | `Atypical.TechnicalAnalysis.Common` | +| `TACore` (the global settings holder) | `TechnicalAnalysis` | `Atypical.TechnicalAnalysis.Common` | + +Note the last row: **`TACore` lives in the bare `TechnicalAnalysis` namespace**, not in +`TechnicalAnalysis.Common`. A typical file starts with: + +```csharp +using TechnicalAnalysis; // TACore +using TechnicalAnalysis.Candles; // TACandle +using TechnicalAnalysis.Common; // RetCode, MAType, FuncUnstId, ... +using TechnicalAnalysis.Functions; // TAMath, TAFunc, SmaResult, ... +``` + +### Target framework + +The packages target `net10.0` only. `LangVersion` is 14, nullable reference types are enabled, and +the candlestick API uses generic math (`System.Numerics.IFloatingPoint`), which requires .NET 7 +or later at minimum. + +--- + +## 2. Your first indicator in five lines + +```csharp +using TechnicalAnalysis.Functions; + +double[] close = [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]; +SmaResult sma = TAMath.Sma(0, close.Length - 1, close, timePeriod: 5); +Console.WriteLine($"RetCode={sma.RetCode} BegIdx={sma.BegIdx} NBElement={sma.NBElement}"); +Console.WriteLine(string.Join(", ", sma.Real)); +``` + +Real output: + +```text +RetCode=Success BegIdx=4 NBElement=6 +12, 13, 14, 15, 16, 17, 0, 0, 0, 0 +``` + +Look at that second line carefully. `sma.Real` has **10** elements β€” the same length as the input β€” +but only the **first 6** are real SMA values, and the first of them belongs to input bar **4**, not +bar 0. The trailing four zeros are uninitialised slack. + +That is the whole game. Read the next section before writing any trading logic. + +--- + +## 3. `RetCode`, `BegIdx`, `NBElement` β€” the part that actually matters + +### 3.1 Anatomy of a result + +Every `TAMath.*` and `TACandle.*` method returns a record deriving from +`TechnicalAnalysis.Common.IndicatorResult`: + +```csharp +public abstract record IndicatorResult +{ + public RetCode RetCode { get; } // Success, BadParam, OutOfRangeStartIndex, OutOfRangeEndIndex, InternalError + public int BegIdx { get; } // index, in the INPUT array, of the first valid output element + public int NBElement { get; } // how many output elements are valid (note: capital B) +} +``` + +- `SingleOutputResult` adds `public double[] Real { get; }`. +- `DualOutputResult` / `TripleOutputResult` keep `Real0` / `Real1` / `Real2` **protected**; the + concrete record exposes named properties (`MacdValue`, `RealUpperBand`, `SlowK`, ...). +- `CandleIndicatorResult` adds `public int[] Integers { get; }` with values `0`, `+100` or `-100`. + +`TAMath` allocates every output array as `new double[endIdx - startIdx + 1]` and fills it **from +index 0**. So: + +> **Output element `k` corresponds to input bar `BegIdx + k`, for `k` in `[0, NBElement)`.** +> Everything at index `>= NBElement` is meaningless. + +Getting this wrong silently shifts every signal in time. It is the single most common correctness +bug when consuming this library, and it never throws. + +> **You can have this arithmetic done for you.** The [fluent API](fluent-api.md) β€” +> `PriceSeries.FromClose(closes).Sma(30)` β€” returns an `IndicatorSeries` in which every index is a +> *bar* index and a bar that has not warmed up yet is `null`. It owns the `BegIdx`/`NBElement` +> conversion in one tested place, so no call site can get it wrong. Read the rest of this section +> anyway: you need it to reach the raw layer, to debug, and to understand what the fluent layer is +> protecting you from. + +### 3.2 A fully worked example + +Eight daily bars, a 3-period SMA: + +```csharp +using TechnicalAnalysis.Functions; + +DateTime[] dates = +[ + new(2024, 1, 2), new(2024, 1, 3), new(2024, 1, 4), new(2024, 1, 5), + new(2024, 1, 8), new(2024, 1, 9), new(2024, 1, 10), new(2024, 1, 11) +]; +double[] close = [100.0, 102.0, 101.0, 105.0, 107.0, 106.0, 110.0, 111.0]; + +SmaResult sma = TAMath.Sma(0, close.Length - 1, close, timePeriod: 3); + +Console.WriteLine($"Real.Length={sma.Real.Length} RetCode={sma.RetCode} BegIdx={sma.BegIdx} NBElement={sma.NBElement}"); + +for (int k = 0; k < sma.NBElement; k++) +{ + int bar = sma.BegIdx + k; + Console.WriteLine($"k={k} bar={bar} date={dates[bar]:yyyy-MM-dd} close={close[bar]} sma={sma.Real[k]}"); +} +``` + +Real output: + +```text +Real.Length=8 RetCode=Success BegIdx=2 NBElement=6 +k=0 bar=2 date=2024-01-04 close=101 sma=101 +k=1 bar=3 date=2024-01-05 close=105 sma=102.66666666666667 +k=2 bar=4 date=2024-01-08 close=107 sma=104.33333333333333 +k=3 bar=5 date=2024-01-09 close=106 sma=106 +k=4 bar=6 date=2024-01-10 close=110 sma=107.66666666666667 +k=5 bar=7 date=2024-01-11 close=111 sma=109 +``` + +Cross-check the arithmetic by hand: + +| Bar | Date | Close | Window | SMA(3) | Output slot | +| ---: | --- | ---: | --- | ---: | --- | +| 0 | 2024-01-02 | 100 | β€” | warm-up | β€” | +| 1 | 2024-01-03 | 102 | β€” | warm-up | β€” | +| 2 | 2024-01-04 | 101 | (100 + 102 + 101) / 3 | 101 | `Real[0]` | +| 3 | 2024-01-05 | 105 | (102 + 101 + 105) / 3 | 102.666… | `Real[1]` | +| 4 | 2024-01-08 | 107 | (101 + 105 + 107) / 3 | 104.333… | `Real[2]` | +| 5 | 2024-01-09 | 106 | (105 + 107 + 106) / 3 | 106 | `Real[3]` | +| 6 | 2024-01-10 | 110 | (107 + 106 + 110) / 3 | 107.666… | `Real[4]` | +| 7 | 2024-01-11 | 111 | (106 + 110 + 111) / 3 | 109 | `Real[5]` | + +The raw array is: + +```text +[101, 102.66666666666667, 104.33333333333333, 106, 107.66666666666667, 109, 0, 0] + |<------------------- NBElement = 6 valid elements, bars 2..7 ----------->| |<-->| + index >= NBElement: + padding, not data +``` + +`BegIdx == 2` because a 3-period SMA needs 2 prior bars. You can ask for that number up front: + +```csharp +int lookback = TAFunc.SmaLookback(3); // => 2 +``` + +Every indicator has a matching `TAFunc.Lookback(...)`. Measured values: + +| Call | Lookback | +| --- | ---: | +| `TAFunc.SmaLookback(3)` | 2 | +| `TAFunc.EmaLookback(30)` | 29 | +| `TAFunc.RsiLookback(14)` | 14 | +| `TAFunc.AtrLookback(14)` | 14 | +| `TAFunc.AdxLookback(14)` | 27 | +| `TAFunc.MacdLookback(12, 26, 9)` | 33 | +| `TAFunc.BollingerBandsLookback(20, MAType.Sma)` | 19 | +| `TAFunc.StochLookback(5, 3, MAType.Sma, 3, MAType.Sma)` | 8 | +| `TAFunc.CciLookback(20)` | 19 | +| `TAFunc.ObvLookback()` | 0 | + +### 3.3 Aligning an indicator back onto the price series + +Charting libraries, joins and backtests all want an array parallel to the bar array. Write the +projection once and never do it inline again: + +```csharp +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +public static class IndicatorAlignment +{ + /// + /// Projects a single-output indicator back onto the original bar index space, + /// padding the warm-up region with . + /// + public static double[] AlignTo(this SingleOutputResult result, int barCount) + { + ArgumentNullException.ThrowIfNull(result); + + double[] aligned = new double[barCount]; + Array.Fill(aligned, double.NaN); + + if (result.RetCode != RetCode.Success) + { + return aligned; + } + + for (int k = 0; k < result.NBElement; k++) + { + aligned[result.BegIdx + k] = result.Real[k]; + } + + return aligned; + } +} +``` + +```csharp +double[] aligned = TAMath.Sma(0, close.Length - 1, close, timePeriod: 3).AlignTo(close.Length); +Console.WriteLine(string.Join(", ", aligned.Select(v => double.IsNaN(v) ? "NaN" : v.ToString("0.####")))); +``` + +Real output: + +```text +NaN, NaN, 101, 102.6667, 104.3333, 106, 107.6667, 109 +``` + +Now `aligned[i]` and `close[i]` refer to the same bar, and `double.NaN` marks "not computable yet" +so it can never be mistaken for a legitimate `0.0` reading β€” which matters enormously for +oscillators such as MACD or CCI that genuinely cross zero. + +Prefer `NaN` padding over `0.0` padding; prefer both over silently reusing `Real` as if it were +bar-aligned. + +The library ships this projection so you do not have to write it: +`IndicatorSeries.ToBarAlignedArray()` is the `NaN`-padded version above, and +`ToBarAlignedNullableArray()` is the `double?[]` version that has no sentinel at all. See +[the fluent API guide](fluent-api.md#projections-onto-the-bar-axis). + +### 3.4 `startIdx` does not clip the input array + +This surprises everyone. `startIdx` is where you want output to *begin*, not where the data begins. +The function still reads bars **before** `startIdx` when they exist: + +```csharp +SmaResult subset = TAMath.Sma(2, 7, close, timePeriod: 3); +Console.WriteLine($"Real.Length={subset.Real.Length} BegIdx={subset.BegIdx} NBElement={subset.NBElement}"); +Console.WriteLine(string.Join(", ", subset.Real)); +``` + +Real output: + +```text +Real.Length=6 BegIdx=2 NBElement=6 +101, 102.66666666666667, 104.33333333333333, 106, 107.66666666666667, 109 +``` + +Six outputs starting at bar 2 β€” including bar 2 itself, whose SMA needed bars 0 and 1. Contrast +with `TAMath.Sma(0, 7, close, 3)` from Β§3.2, which also produced `BegIdx = 2`: with `startIdx = 0` +the engine clamps `startIdx` up to the lookback; with `startIdx = 2` it already has enough history. + +Two consequences: + +- **Do not slice your price array to "save work".** Slicing destroys the history the indicator + needs and moves `BegIdx`. Pass the full array and set `startIdx`/`endIdx` instead. +- `Real.Length == endIdx - startIdx + 1`, always, whatever `NBElement` turns out to be. +- `BegIdx == max(startIdx, lookback)`. It is an index into the **input** array, never an offset + relative to `startIdx`, and never less than `startIdx`. + +### 3.5 `RetCode` is necessary, not sufficient + +```csharp +SmaResult tooShort = TAMath.Sma(0, 1, [1.0, 2.0], timePeriod: 30); +Console.WriteLine($"{tooShort.RetCode} BegIdx={tooShort.BegIdx} NBElement={tooShort.NBElement}"); + +SmaResult badPeriod = TAMath.Sma(0, 7, close, timePeriod: 1); +Console.WriteLine($"{badPeriod.RetCode} NBElement={badPeriod.NBElement}"); + +SmaResult badStart = TAMath.Sma(-1, 5, close, timePeriod: 3); +Console.WriteLine(badStart.RetCode); +``` + +Real output: + +```text +Success BegIdx=0 NBElement=0 +BadParam NBElement=0 +OutOfRangeStartIndex +``` + +**Not enough data is `Success` with `NBElement == 0`, not an error.** Always guard on both: + +```csharp +if (result.RetCode != RetCode.Success || result.NBElement == 0) +{ + return; // nothing usable +} +``` + +The full `RetCode` enum is small: + +| Value | Numeric | Meaning | +| --- | ---: | --- | +| `RetCode.Success` | 0 | Call completed. `NBElement` may still be `0`. | +| `RetCode.BadParam` | 2 | A period is out of range (valid range is 2..100000 for most), or an input array is `null`. | +| `RetCode.OutOfRangeStartIndex` | 12 | `startIdx < 0`. | +| `RetCode.OutOfRangeEndIndex` | 13 | `endIdx < 0` or `endIdx < startIdx`. | +| `RetCode.InternalError` | 5000 | Should not happen; report it. | + +--- + +## 4. Multi-output indicators + +`Real0` / `Real1` / `Real2` are `protected`. Use the named properties: + +```csharp +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +double[] high = /* ... */; +double[] low = /* ... */; +double[] close = /* ... */; +int last = close.Length - 1; + +MacdResult macd = TAMath.Macd(0, last, close, + optInFastPeriod: 12, optInSlowPeriod: 26, optInSignalPeriod: 9); +// macd.MacdValue, macd.MacdSignal, macd.MacdHist + +BollingerBandsResult bb = TAMath.BollingerBands(0, last, close, + timePeriod: 20, nbDevUp: 2.0, nbDevDn: 2.0, maType: MAType.Sma); +// bb.RealUpperBand, bb.RealMiddleBand, bb.RealLowerBand + +StochResult stoch = TAMath.Stoch(0, last, high, low, close, + fastKPeriod: 5, slowKPeriod: 3, slowKMAType: MAType.Sma, + slowDPeriod: 3, slowDMAType: MAType.Sma); +// stoch.SlowK, stoch.SlowD +``` + +All output arrays of one result share the **same** `BegIdx` and `NBElement`. You align them once. + +A few results are `int[]` rather than `double[]`: `MaxIndexResult.Integers`, `MinIndexResult.Integers`, +`HtTrendModeResult.Integers`, and `MinMaxIndexResult.MinIdx` / `.MaxIdx`. See the +[indicator reference](../indicators/README.md) for the exact output property names of all 159 entry +points. + +--- + +## 5. Choosing `double` vs `float` + +Almost every `TAMath` method is declared twice β€” once with `double[]` inputs, once with `float[]`: + +```csharp +public static SmaResult Sma(int startIdx, int endIdx, double[] real, int timePeriod = 30); +public static SmaResult Sma(int startIdx, int endIdx, float[] real, int timePeriod = 30); +``` + +The `float[]` overload is a thin adapter: it widens each input array to a freshly allocated +`double[]` and calls the `double[]` implementation. Therefore: + +- **Computation is always in `double`.** There is no reduced-precision code path. +- **Outputs are always `double[]`** (or `int[]`), whatever the input type. `SmaResult.Real` is + `double[]` even when you called the `float[]` overload. +- The `float[]` overload **costs one extra array allocation and copy per input array**. + +Rules of thumb: + +- Store and pass `double[]` unless something outside your control hands you `float[]`. +- Never round-trip `double[] -> float[] -> TAMath` to "save memory"; you pay an allocation and lose + precision for nothing. +- If your source of truth is `decimal` (an exchange API, a ledger), convert to `double` once at the + boundary for `TAMath`. For `TACandle`, you do not need to convert at all β€” see the next section. + +The `float[]` overload is not a compatibility shim you should feel bad about using; it exists so +that pipelines already holding `float[]` (GPU buffers, interop, compact caches) do not need a manual +conversion step. It just is not faster. + +> One sharp edge: because both overloads exist, `TAMath.Sma(0, 7, null, 3)` does not compile β€” +> `CS0121: the call is ambiguous`. Cast the `null`, or better, do not pass `null` (it returns +> `RetCode.BadParam` anyway). + +--- + +## 6. Candlestick patterns with generic math + +`TACandle` is generic over `T : IFloatingPoint`: + +```csharp +public static CandleIndicatorResult CdlDoji( + int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close) + where T : IFloatingPoint; +``` + +`double`, `float` and `decimal` all satisfy that constraint (as do `Half` and `NFloat`), so a +`decimal[]` price series β€” the natural representation for exchange data β€” runs through pattern +recognition with **no conversion**: + +```csharp +using TechnicalAnalysis.Candles; +using TechnicalAnalysis.Common; + +decimal[] open = [100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 100m, 105m, 98m, 100m]; +decimal[] high = [101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 106m, 107m, 101m]; +decimal[] low = [ 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 99m, 97m, 99m]; +decimal[] close = [101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 101m, 99m, 106m, 100m]; + +CandleIndicatorResult engulfing = TACandle.CdlEngulfing(0, close.Length - 1, open, high, low, close); + +Console.WriteLine($"RetCode={engulfing.RetCode} BegIdx={engulfing.BegIdx} NBElement={engulfing.NBElement}"); +for (int k = 0; k < engulfing.NBElement; k++) +{ + if (engulfing.Integers[k] != 0) + { + Console.WriteLine($"bar {engulfing.BegIdx + k}: {engulfing.Integers[k]}"); + } +} +``` + +Real output: + +```text +RetCode=Success BegIdx=2 NBElement=12 +bar 11: -100 +bar 12: 100 +``` + +Bar 11 is a bearish engulfing (`-100`), bar 12 a bullish one (`+100`). Running the identical series +as `double[]` and as `float[]` produced byte-identical `Integers` arrays. + +Key facts: + +- `CandleIndicatorResult.Integers` is `int[]`, and it obeys the **same** `BegIdx` / `NBElement` + contract as `Real`. `Integers.Length` is `endIdx - startIdx + 1`; only `NBElement` entries are + valid, starting at input bar `BegIdx`. +- `0` = pattern absent, `+100` = bullish, `-100` = bearish. Only the direction-aware patterns emit + both signs β€” `CdlEngulfing` derives the sign from the candle colour. Single-direction patterns emit + one sign only: `Cdl2Crows` emits `0`/`-100`, `CdlHammer` emits `0`/`+100`, and non-directional + patterns such as `CdlDoji` emit `0`/`+100` where `+100` just means "present". Test `!= 0` when you + only care about presence. +- Seven patterns take an extra `T penetration` argument and also expose a no-penetration overload: + `CdlAbandonedBaby`, `CdlDarkCloudCover`, `CdlEveningDojiStar`, `CdlEveningStar`, `CdlMatHold`, + `CdlMorningDojiStar`, `CdlMorningStar`. Because the parameter is `T`, you write `0.3m` for + `decimal[]` inputs and `0.3` for `double[]` inputs: + + ```csharp + CandleIndicatorResult ms = TACandle.CdlMorningStar(0, close.Length - 1, open, high, low, close, 0.3m); + CandleIndicatorResult msDefault = TACandle.CdlMorningStar(0, close.Length - 1, open, high, low, close); + ``` + +- Pattern lookbacks are driven by the **global candle settings**, not only by the bar geometry. A + `CdlDoji` on the 14-bar series above returned `BegIdx = 10`, because `CandleSettingType.BodyDoji` + averages the body over 10 bars by default. Short series legitimately produce `NBElement == 0`. + +--- + +## 7. Tuning global behaviour via `TACore.Globals` + +`TACore` (namespace `TechnicalAnalysis`) exposes a single process-wide `GlobalsType` instance with +three knobs. + +### 7.1 Unstable periods + +Recursive indicators (EMA, RSI, ADX, ATR, the Hilbert Transform family, …) never fully forget their +seed value. TA-Lib lets you discard an extra warm-up window so results converge to the +"steady-state" value regardless of where your data starts. + +```csharp +using TechnicalAnalysis; +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +Console.WriteLine(TACore.Globals.UnstablePeriod[FuncUnstId.Ema]); // 0 by default +Console.WriteLine(TAFunc.EmaLookback(30)); // 29 + +TACore.Globals.UnstablePeriod[FuncUnstId.Ema] = 100; +Console.WriteLine(TAFunc.EmaLookback(30)); // 129 +``` + +On a 200-bar series, `TAMath.Ema(0, 199, prices, timePeriod: 30)` moved from +`BegIdx = 29, NBElement = 171` to `BegIdx = 129, NBElement = 71` β€” the first 100 outputs were +discarded, and the ones that remain no longer depend on the arbitrary seed. + +`UnstablePeriod` is a `Dictionary` pre-populated with `0` for all 23 ids: +`Adx`, `Adxr`, `Atr`, `Cmo`, `Dx`, `Ema`, `HtDcPeriod`, `HtDcPhase`, `HtPhasor`, `HtSine`, +`HtTrendline`, `HtTrendMode`, `Kama`, `Mama`, `Mfi`, `MinusDI`, `MinusDM`, `Natr`, `PlusDI`, +`PlusDM`, `Rsi`, `StochRsi`, `T3` (plus the sentinels `FuncUnstNone = -1` and `FuncUnstAll = 23`, +which are **not** usable as keys β€” there is no "set them all" helper; loop over the ids you care +about). + +When to raise it: when you compare against a chart whose history starts much earlier than yours, or +when you want two runs over different date ranges to agree on the overlap. Typical values are +`50`–`250`. The cost is a shorter output series. + +### 7.2 Compatibility mode + +```csharp +TACore.Globals.Compatibility = Compatibility.Metastock; // default is Compatibility.Default +``` + +`Compatibility.Metastock` seeds EMA with the first input value and shifts the RSI/CMO lookback by +one. Only switch if you are explicitly reproducing MetaStock output. + +`Compatibility.Default` seeds EMA with the simple average of the first `timePeriod` values, as the C +library does: `TAMath.Ema(0, 7, [100, 102, 101, 105, 107, 106, 110, 111], timePeriod: 3)` returns +`Real[0] = 101`, the mean of `100`, `102` and `101`. + +Seeding differences decay geometrically but never vanish β€” see the +[TradingView parity caveats](tradingview-integration.md#2-parity-caveats). + +### 7.3 Candle settings + +Candlestick detection thresholds are data, not constants: + +```csharp +using TechnicalAnalysis; +using TechnicalAnalysis.Common; + +CandleSetting bodyDoji = TACore.Globals.CandleSettings[CandleSettingType.BodyDoji]; +// RangeType=HighLow, AvgPeriod=10, Factor=0.1 + +TACore.Globals.CandleSettings[CandleSettingType.BodyDoji] = + new CandleSetting(CandleSettingType.BodyDoji, RangeType.HighLow, 10, 0.05); +``` + +The eleven tunable settings are `BodyLong`, `BodyVeryLong`, `BodyShort`, `BodyDoji`, `ShadowLong`, +`ShadowVeryLong`, `ShadowShort`, `ShadowVeryShort`, `Near`, `Far`, `Equal` +(`CandleSettingType.AllCandleSettings` is a sentinel, not a key). `CandleSetting.GetDefaultByType` +and the `CandleSetting.Default*` static properties give you the stock values back. + +### 7.4 `TACore.Globals` is process-wide mutable state + +There is no locking and no scoping. Set it once during start-up, before any indicator runs, and +treat it as immutable afterwards. Mutating it from a request handler while other requests compute +indicators produces non-deterministic results. If you need per-tenant settings, run separate +processes. + +--- + +## 8. The low-level `TAFunc` API + +`TAMath` is a friendly wrapper. `TAFunc` is the direct port, and it lets you own the output buffer β€” +useful in hot loops where you want to reuse arrays instead of allocating one per call: + +```csharp +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +double[] close = [100.0, 102.0, 101.0, 105.0, 107.0, 106.0, 110.0, 111.0]; + +int outBegIdx = 0; +int outNBElement = 0; +double[] outReal = new double[close.Length]; + +RetCode rc = TAFunc.Sma(0, close.Length - 1, close, 3, ref outBegIdx, ref outNBElement, ref outReal); +Console.WriteLine($"rc={rc} outBegIdx={outBegIdx} outNBElement={outNBElement} outReal[0]={outReal[0]}"); +``` + +Real output: + +```text +rc=Success outBegIdx=2 outNBElement=6 outReal[0]=101 +``` + +The `in`/`ref` signature is `TAFunc.Sma(int startIdx, int endIdx, in double[] inReal, in int optInTimePeriod, ref int outBegIdx, ref int outNBElement, ref double[] outReal)`. +The same `BegIdx` / `NBElement` contract applies β€” `outReal[k]` is bar `outBegIdx + k`. + +`TAFunc` also owns the `*Lookback` helpers used in Β§3.2. + +--- + +## 9. Common pitfalls + +**1. Treating `Real[i]` as bar `i`.** It is bar `BegIdx + i`. Always project through `BegIdx`. + +**2. Iterating to `Real.Length` instead of `NBElement`.** The tail is zeros, and `0.0` is a perfectly +plausible value for MACD, CCI, ROC or a correlation. Loop to `NBElement`. + +**3. Treating `RetCode.Success` as "I have data".** Insufficient history returns `Success` with +`NBElement == 0`. Guard on both. + +**4. Passing `endIdx >= array.Length`.** The library validates index *ordering* and array *nullness*, +but not `endIdx` against the array length. `TAMath.Sma(0, 100, eightBars, 3)` throws +`IndexOutOfRangeException`. Always pass `endIdx: prices.Length - 1`. + +**5. Passing `startIdx > endIdx`.** `TAMath` computes `new double[endIdx - startIdx + 1]`, so a +reversed range throws `OverflowException` before any validation runs. Check your range first. + +**6. Slicing the price array to the window you care about.** That deletes the lookback history and +changes the answer. Pass the whole array with a narrower `startIdx`. + +**7. Assuming `float[]` is cheaper.** It allocates a `double[]` copy of every input array and +computes in `double` regardless. + +**8. Forgetting that unstable periods change `BegIdx`.** After raising +`TACore.Globals.UnstablePeriod[...]`, cached `Lookback` values and any hard-coded warm-up offsets are +wrong. Re-read `BegIdx` from the result. + +**9. Mutating `TACore.Globals` concurrently.** See Β§7.4. + +**10. Comparing against a chart without matching the history window.** Recursive indicators depend on +how far back the data starts. Two "RSI(14)" series computed from different start dates differ +slightly forever; that is mathematics, not a bug. See the +[TradingView parity caveats](tradingview-integration.md#2-parity-caveats). + +**11. Reusing a result record after mutating the input array.** Results hold references to arrays +they allocated, but the inputs are read at call time. Recompute after any input change; there is no +incremental/streaming API. + +**12. Assuming candlestick patterns work on tiny series.** Pattern lookbacks include the +`CandleSettings` averaging window (10 bars for `BodyDoji`, for example). Feed at least ~30 bars. + +**13. Charting a value without checking `double.IsFinite`.** `Success` and a non-zero `NBElement` +do not imply a finite number β€” a non-finite *input* propagates through every running sum and every +recursion. One `NaN` or `∞` in a series poisons every aggregate computed from it. (The fluent API +refuses non-finite prices at the boundary for exactly this reason; see the +[fluent API guide](fluent-api.md#non-finite-prices-are-refused-at-the-boundary).) + +--- + +## 10. Where to go next + +- [The fluent API](fluent-api.md) β€” `PriceSeries` and `IndicatorSeries`, the bar-indexed layer that + does the `BegIdx`/`NBElement` arithmetic of Β§3 for you. +- [Complete indicator reference](../indicators/README.md) β€” all 159 entry points with signatures, + parameters, defaults, outputs and links to the generated API pages. +- [TradingView integration](tradingview-integration.md) β€” Pine Script mapping, parity caveats, + serving series to a chart, consuming webhook alerts. +- [`Atypical.TechnicalAnalysis.Functions` API reference](../functions/Atypical.TechnicalAnalysis.Functions.md) +- [`Atypical.TechnicalAnalysis.Candles` API reference](../candles/Atypical.TechnicalAnalysis.Candles.md) +- [`Atypical.TechnicalAnalysis.Common` API reference](../common/Atypical.TechnicalAnalysis.Common.md) diff --git a/docs/guides/real-time-streaming.md b/docs/guides/real-time-streaming.md new file mode 100644 index 00000000..417e95e7 --- /dev/null +++ b/docs/guides/real-time-streaming.md @@ -0,0 +1,745 @@ +# πŸ“‘ Real-time streaming indicators + +TA-Lib is a batch API: every function takes a whole array and gives you a whole array back. A live feed +is not a whole array. This guide is about the gap between those two facts, and about the sample that +closes it. + +The sample lives in two projects: + +| Project | What it is | +|---------|------------| +| [`samples/TechnicalAnalysis.Samples.RealTime`](../../samples/TechnicalAnalysis.Samples.RealTime) | ASP.NET Core server: synthetic tick feed β†’ OHLCV bars β†’ seven indicators (eleven series) per closed bar β†’ SignalR **and** raw WebSocket, plus a zero-dependency browser dashboard | +| [`samples/TechnicalAnalysis.Samples.RealTime.Client`](../../samples/TechnicalAnalysis.Samples.RealTime.Client) | Console SignalR client that renders snapshots, exercising both server push paths | + +Everything is offline and deterministic. There is no market data provider, no API key, no network egress. + +--- + +## πŸ“ Table of contents + + +* [πŸ“‘ Real-time streaming indicators](#-real-time-streaming-indicators) + * [πŸ“ Table of contents](#-table-of-contents) + * [🏁 Run it](#-run-it) + * [The server](#the-server) + * [The browser dashboard](#the-browser-dashboard) + * [The console client](#the-console-client) + * [πŸ—οΈ Architecture](#-architecture) + * [The pipeline, stage by stage](#the-pipeline-stage-by-stage) + * [πŸ”Œ Transports](#-transports) + * [HTTP endpoints](#http-endpoints) + * [SignalR hub](#signalr-hub) + * [Raw WebSocket](#raw-websocket) + * [πŸ“¦ Message contracts](#-message-contracts) + * [🌑️ Warm-up and null semantics](#-warm-up-and-null-semantics) + * [The alignment rule, which is the whole ballgame](#the-alignment-rule-which-is-the-whole-ballgame) + * [The measured warm-up ladder](#the-measured-warm-up-ladder) + * [βš™οΈ Configuration](#-configuration) + * [πŸ› Known library defects visible in this sample](#-known-library-defects-visible-in-this-sample) + * [🏭 Production notes](#-production-notes) + * [SignalR scale-out and the Redis backplane](#signalr-scale-out-and-the-redis-backplane) + * [Per-connection state cost](#per-connection-state-cost) + * [Backpressure: bounded channels, drop oldest](#backpressure-bounded-channels-drop-oldest) + * [The push transport needs its own bound](#the-push-transport-needs-its-own-bound) + * [Cancellation and disconnect handling](#cancellation-and-disconnect-handling) + * [When to move to a true incremental indicator](#when-to-move-to-a-true-incremental-indicator) + * [Things this sample deliberately does not do](#things-this-sample-deliberately-does-not-do) + + +--- + +## 🏁 Run it + +### The server + +```shell +# from the repository root β€” defaults to 5-second bars, 250 ms ticks +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime -c Release +``` + +The listen URL lives in `appsettings.json` (`"Urls": "http://localhost:5199"`) rather than in +`launchSettings.json`, because `**/Properties/launchSettings.json` is gitignored repo-wide and would not +survive a clone. + +For a demo you do not want to wait five seconds a bar for. Every option is overridable on the command +line: + +```shell +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime -c Release -- \ + --RealTime:BarSeconds=1 --RealTime:TickIntervalMilliseconds=100 +``` + +Startup log from a real run with those overrides: + +```text +info: TechnicalAnalysis.Samples.RealTime.Streaming.SyntheticMarketDataFeed[1000] + Synthetic feed starting for 3 symbol(s) at one tick every 100 ms. +info: TechnicalAnalysis.Samples.RealTime.Streaming.IndicatorPipeline[1100] + Indicator pipeline running: 1s bars, 256-bar window, 34 bars until every indicator is warm. +info: Microsoft.Hosting.Lifetime[14] + Now listening on: http://localhost:5199 +``` + +Check it is alive: + +```shell +curl -s http://localhost:5199/health +``` + +```json +{"status":"healthy","serverTimeUtc":"2026-07-27T12:38:06.001382+00:00","uptimeSeconds":15.4, + "symbols":["ACME","GLOBEX","INITECH"],"ticksPublished":459,"barsClosed":45, + "tickSubscribers":1,"snapshotSubscribers":0,"groupSubscribers":0,"pushesDropped":0} +``` + +`snapshotSubscribers` and `groupSubscribers` count **different** things, and the split is deliberate: + +* `snapshotSubscribers` counts *channel* subscribers β€” raw WebSocket clients and `StreamSnapshots` + callers, i.e. everything holding a `BoundedFanout` subscription. +* `groupSubscribers` counts SignalR **push** subscriptions, one per connection per subscribed symbol. + A connection that called `Subscribe` lives in a hub group and never touches a fanout, so it is + invisible to the first counter. Reporting one number for both would let a leaked group membership + hide behind a healthy-looking zero. +* `pushesDropped` counts bars the push transport gave up on. Non-zero means some subscriber stopped + reading; see [Backpressure](#backpressure-bounded-channels-drop-oldest). + +### The browser dashboard + +Open . `UseDefaultFiles` + `UseStaticFiles` serve +`wwwroot/index.html`, a single self-contained file with no external dependencies: a canvas price line +with the two SMA overlays, a separate RSI pane with a shaded 30/70 corridor, a symbol picker, a live +status dot, exponential-backoff reconnect and a tape of recent bars. + +It talks to the **raw WebSocket** endpoint, not to SignalR β€” which is the point: the raw endpoint has to +be good enough for a real consumer, so the sample's own page uses it. + +The signature detail is the warm-up rendering. An indicator that has not produced a value yet draws as a +dashed rule with a hatched hairline that fills as bars accumulate, plus a line like +`22 more bar(s) before MACD prints.` The null-versus-warm distinction is the thing this sample most wants +you to internalise, so it is the thing the page draws most loudly. + +The page builds its socket URL from the page's own origin β€” +`` `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws/indicators${query}` `` β€” so +it is same-origin by construction and needs no configuration. To point it at a different server, change +that one expression in `wwwroot/index.html` (and remember that a cross-origin WebSocket is not subject to +CORS preflight, so the *server* must check `Origin` if you care). + +### The console client + +With the server running, in a second terminal: + +```shell +# push transport (SignalR groups), the server's first symbol, until Ctrl+C +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime.Client -c Release + +# a specific symbol, the hub's streaming method, and a time limit +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime.Client -c Release -- \ + --stream --symbol GLOBEX --seconds 8 + +# a server somewhere else β€” a root or a full hub URL both work +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime.Client -c Release -- \ + http://192.168.1.20:5199 +``` + +Real `--help` output: + +```text +Usage: dotnet run [-- ] [options] + + Server root or full hub URL. Default: http://localhost:5199 + --symbol Symbol to follow. Default: the server's first symbol. + --stream Use the hub's streaming method instead of the group push. + --seconds Exit after n seconds instead of running until Ctrl+C. + --help Show this text. +``` + +An interactive terminal gets an ANSI table repainted in place, with a colour-coded signal and dashed +values for nulls. Redirected output gets one greppable line per bar instead β€” chosen with +`Console.IsOutputRedirected`, so piping the client into `grep` produces something useful rather than a +screenful of escape codes. + +The client deliberately declares its own `IndicatorSnapshotDto` rather than referencing the server +project. That way running it genuinely tests the wire contract instead of testing a shared type. + +--- + +## πŸ—οΈ Architecture + +```mermaid +flowchart TB + subgraph feed["SyntheticMarketDataFeed β€” BackgroundService"] + timer["PeriodicTimer
TickIntervalMilliseconds"] --> walk["per-symbol seeded
mean-reverting walk"] + end + + walk -->|"Tick"| tickfan["BoundedFanout of Tick
capacity 64, DropOldest"] + + subgraph pipeline["IndicatorPipeline β€” BackgroundService, one loop"] + direction TB + agg["BarAggregator, per symbol
Unix ms floored to BarSeconds"] + eng["RollingIndicatorEngine, per symbol
256-bar ring, recompute per bar"] + agg -->|"closed Bar"| eng + end + + tickfan -->|"one subscriber"| agg + + subgraph bc["SnapshotBroadcaster"] + fan["BoundedFanout of IndicatorSnapshot
one per symbol"] + cache["last-snapshot cache
per symbol"] + end + + eng -->|"IndicatorSnapshot"| fan + eng -->|"IndicatorSnapshot"| cache + + fan -->|"pull: channel reader"| ws["GET /ws/indicators
IndicatorWebSocketHandler"] + fan -->|"pull: channel reader"| stream["IndicatorHub.StreamSnapshots"] + eng -->|"push: group send"| hub["IHubContext of IndicatorHub
group symbol:SYMBOL"] + + ws --> browser["wwwroot/index.html"] + stream --> console2["console client, --stream"] + hub --> console1["console client, default"] +``` + +### The pipeline, stage by stage + +**1. `SyntheticMarketDataFeed`** β€” a `BackgroundService` on a `PeriodicTimer`. Each symbol gets a seeded +mean-reverting geometric random walk. The per-symbol seed mixes `RandomSeed` with an FNV-1a hash of the +symbol name, *not* `string.GetHashCode`, which is randomised per process and would destroy +reproducibility across restarts. It is registered as a singleton **and** as a hosted service, so +`Subscribe()` reaches the same instance that the host is driving. + +**2. `BoundedFanout`** β€” one bounded channel per subscriber, `BoundedChannelFullMode.DropOldest`. +Publishing takes no lock: the subscriber array is copy-on-write, so `Publish` reads one immutable +snapshot while `Subscribe`/remove lock and swap a new array in. + +**3. `BarAggregator`** β€” one per symbol, buckets ticks on **absolute time**: the tick's Unix millisecond +timestamp floored to the bar period (with floor-mod, so pre-epoch timestamps bucket downwards too). Two +servers started seconds apart therefore agree on bar boundaries. A bar closes when the first tick of a +*later* bucket arrives; late ticks are dropped rather than rewriting a bar a client has already charted. +`Flush()` closes the partial bar on shutdown β€” `IndicatorPipeline` calls it for every symbol once its read +loop exits, so the last, incomplete period is published rather than silently dropped. + +One consequence worth stating: **a period with no ticks produces no bar.** With the synthetic feed that +never happens; against a real venue at 03:00 it happens constantly. Downstream code must treat bar +timestamps as a sparse series, not a metronome. + +**4. `RollingIndicatorEngine`** β€” one per symbol, a ring buffer of the last `WindowSize` bars. On each +closed bar it unrolls the ring into chronological scratch arrays with at most two `Array.Copy` calls +(steady state allocates nothing beyond the library's own result arrays), then recomputes SMA fast, SMA +slow, EMA, RSI, MACD, Bollinger Bands and ATR over that window. See +[Warm-up and null semantics](#-warm-up-and-null-semantics) for the part that actually matters. + +**5. `SnapshotBroadcaster`** β€” per-symbol fanouts plus a last-snapshot cache, so a client that connects +mid-period sees data immediately instead of waiting a whole bar. Symbol resolution is case-insensitive. + +**6. `IndicatorPipeline`** β€” a single `BackgroundService` loop owns every aggregator and engine, which is +why those types can stay simple, single-threaded and lock-free. It publishes each snapshot twice: to the +broadcaster (which the pull transports read) and to a bounded queue drained by a separate pump that does +the SignalR group send (the push transport). Being an ordinary fanout subscriber, the pipeline is subject +to the same drop-oldest policy as everyone else β€” if it ever fell behind the feed it would skip ticks, +producing thinner bars rather than a hung server. **It never awaits a client**; see +[the push transport needs its own bound](#the-push-transport-needs-its-own-bound) for why that matters +more than it looks. + +--- + +## πŸ”Œ Transports + +### HTTP endpoints + +| Route | Method | Returns | +|-------|--------|---------| +| `/` | GET | The dashboard, `text/html` | +| `/health` | GET | `HealthResponse` JSON β€” status, server clock, uptime, symbols, tick and bar counters, channel and group subscriber counts, dropped-push counter | +| `/api/symbols` | GET | `["ACME","GLOBEX","INITECH"]` | +| `/hubs/indicators` | SignalR | The hub | +| `/ws/indicators?symbol=` | WebSocket upgrade | The raw socket. `symbol` is optional | + +A plain `GET` on `/ws/indicators` (no upgrade header) returns **400 Bad Request** with an explanatory +body rather than a stack trace β€” verified. + +### SignalR hub + +Mapped at `/hubs/indicators`. Four server methods, one client method: + +| Wire name | Signature | Notes | +|-----------|-----------|-------| +| `GetSymbols` | `() -> IReadOnlyList` | Configuration order | +| `Subscribe` | `(string symbol) -> Task` | Joins group `symbol:{SYMBOL}` and immediately replays the latest snapshot to the caller. Throws `HubException` for an unknown symbol | +| `Unsubscribe` | `(string symbol) -> Task` | Leaves the group | +| `StreamSnapshots` | `(string symbol, CancellationToken) -> IAsyncEnumerable` | Per-caller bounded queue, per-caller cancellation, disposal guaranteed when the client walks away | +| `snapshot` | *client method*, invoked by the server with one `IndicatorSnapshot` | Both the group push and the replay use this name | + +Symbol matching is case-insensitive. The group name is `symbol:{resolved-symbol}` β€” one send per group +per bar, regardless of how many clients are watching, which is what you want when thousands of browsers +follow the same instrument. + +**Push versus streaming.** Push is connection-scoped: subscribe once and the server broadcasts to the +whole group. Streaming is call-scoped: each caller gets its own queue and its own lifetime. Prefer push +for fan-out, streaming when a single caller wants a request-shaped subscription it can dispose. + +Minimal C# consumer of the push path: + +```csharp +HubConnection connection = new HubConnectionBuilder() + .WithUrl("http://localhost:5199/hubs/indicators") + .WithAutomaticReconnect() + .Build(); + +connection.On("snapshot", snapshot => +{ + Console.WriteLine($"{snapshot.Symbol} #{snapshot.Sequence} rsi={snapshot.Rsi}"); +}); + +// Group membership does not survive a new connection id, so re-subscribe on reconnect. +connection.Reconnected += async _ => await connection.InvokeAsync("Subscribe", "ACME"); + +await connection.StartAsync(); +await connection.InvokeAsync("Subscribe", "ACME"); +``` + +### Raw WebSocket + +`ws://localhost:5199/ws/indicators?symbol=ACME`. Omit `?symbol=` and you get the server's first symbol. +Frames are newline-free JSON objects of `StreamMessage`: one `session` frame, then one `snapshot` frame +per closed bar. + +The handler runs a receive loop concurrently with the send loop under a linked `CancellationTokenSource`. +The receive loop exists even though the protocol is one-way: without it the close handshake never +completes, and a read-only socket is exactly how a server ends up with connections that look alive to it +and dead to everyone else. The close path uses `CloseOutputAsync`, not `CloseAsync`, precisely because +the receive loop owns the read side β€” it then waits up to 5 s for the peer's close frame before giving +up. + +Verified behaviour on the wire: + +```text +$ ws /ws/indicators?symbol=ACME +HTTP/1.1 101 Switching Protocols +{"type":"session", ...} +{"type":"snapshot", ...} + +$ ws /ws/indicators?symbol=NOPE +HTTP/1.1 101 Switching Protocols +{"type":"error","session":null,"snapshot":null, + "error":"Unknown symbol. This server publishes: ACME, GLOBEX, INITECH."} +SERVER CLOSE code=1008 reason=Unknown symbol +``` + +`1008` is `PolicyViolation`: the socket upgrades, tells you what went wrong in a frame you can parse, and +then closes. That is friendlier than refusing the upgrade, because a browser gets no useful information +from a failed handshake. + +--- + +## πŸ“¦ Message contracts + +Copied from +[`Contracts/`](../../samples/TechnicalAnalysis.Samples.RealTime/Contracts). All JSON is camelCase, +produced by a source-generated `JsonSerializerContext` wired into all three exits (minimal API responses, +the SignalR JSON protocol, and the socket handler). + +```csharp +public sealed record IndicatorSnapshot( + string Symbol, + DateTimeOffset Timestamp, // UTC open instant of the bar + decimal Close, + long Sequence, // per-symbol, starts at 1, monotonic + double? SmaFast, + double? SmaSlow, + double? Ema, + double? Rsi, + double? Macd, + double? MacdSignal, + double? MacdHistogram, + double? BollingerUpper, + double? BollingerMiddle, + double? BollingerLower, + double? Atr, + Signal Signal, // serialised as a string + int BarsInWindow, + int BarsRequired) +{ + public bool IsWarmedUp => /* every tracked indicator has a value */; +} + +public sealed record StreamMessage( + string Type, // "session" | "snapshot" | "error" + SessionInfo? Session = null, + IndicatorSnapshot? Snapshot = null, + string? Error = null); + +public sealed record SessionInfo( + string Symbol, + IReadOnlyList Symbols, + int BarSeconds, + int TickIntervalMilliseconds, + int WindowSize, + IndicatorPeriods Periods); +``` + +`Signal` is `Neutral | Bullish | Bearish | Overbought | Oversold`, serialised as a string via +`JsonStringEnumConverter` so a browser or a Python consumer can render it without sharing an enum +definition. + +A real `session` frame, captured from the socket: + +```json +{"type":"session","session":{"symbol":"ACME","symbols":["ACME","GLOBEX","INITECH"], + "barSeconds":1,"tickIntervalMilliseconds":100,"windowSize":256, + "periods":{"smaFast":10,"smaSlow":30,"ema":20,"rsi":14,"macdFast":12,"macdSlow":26, + "macdSignal":9,"bollinger":20,"bollingerDeviations":2,"atr":14}}, + "snapshot":null,"error":null} +``` + +A real `snapshot` frame, same socket, one bar later: + +```json +{"type":"snapshot","session":null,"snapshot":{ + "symbol":"ACME","timestamp":"2026-07-27T12:39:02+00:00","close":341.17,"sequence":73, + "smaFast":340.60900000000015,"smaSlow":340.827,"ema":340.4500705641675, + "rsi":52.09158237803157,"macd":0.2751059392911088,"macdSignal":0.06660160928194875, + "macdHistogram":0.20850433000916008,"bollingerUpper":343.07838314273073, + "bollingerMiddle":339.8940000000001,"bollingerLower":336.7096168572695, + "atr":6.269090362256177e+63, + "signal":"Neutral","barsInWindow":73,"barsRequired":34,"isWarmedUp":true},"error":null} +``` + +That `"atr":6.269090362256177e+63` is not a typo and not a bug in the sample. See +[Known library defects](#-known-library-defects-visible-in-this-sample). + +The `session` frame exists so the page can label its axes and legends from configuration instead of +hard-coding periods. The `Sequence` number exists so a client can *detect* the frames it dropped β€” which +a bounded stream is allowed to do. + +--- + +## 🌑️ Warm-up and null semantics + +Every indicator field is nullable, and **every null means exactly one thing: that indicator has not +produced a value for this bar yet.** A null is never replaced by zero and never by the previous bar's +value, because both of those lies are indistinguishable from a real reading once they are on the wire. + +### The alignment rule, which is the whole ballgame + +`TAMath` allocates its output as `new double[endIdx - startIdx + 1]` and fills it **from index 0**. The +output array is *not* parallel to the input array: + +```text +output[k] describes input bar (BegIdx + k) for k in [0, NBElement) +``` + +Elements from `NBElement` onwards are uninitialised zeros that mean nothing. + +So the newest value is `output[NBElement - 1]` β€” never `output[lastInputIndex]`, which with a 256-bar +window and a 30-bar lookback would read 29 elements past the real data and hand back a zero dressed up as +a price. `RollingIndicatorEngine` does not merely follow that rule, it **asserts** it: + +```csharp +private static double? Latest(IndicatorResult result, double[] output, int lastInputIndex) +{ + if (result.RetCode != RetCode.Success || result.NBElement <= 0) + { + return null; + } + + int newestOutputIndex = result.NBElement - 1; + int newestInputIndex = result.BegIdx + newestOutputIndex; + + if (newestInputIndex != lastInputIndex || newestOutputIndex >= output.Length) + { + // Returning the previous bar's value here is the classic streaming bug: + // it looks like data and is silently one bar stale. + return null; + } + + double value = output[newestOutputIndex]; + return double.IsFinite(value) ? value : null; +} +``` + +Two guards are doing work there. The index check turns "the indicator did not cover this bar" into a null +instead of a stale number. The `IsFinite` check turns a NaN or an infinity into a null, which is why a +flat series does not put `NaN` on the wire even though the underlying RSI can produce one. + +### The measured warm-up ladder + +Captured from a real run against a freshly started server with `--RealTime:BarSeconds=1`, connecting the +console client immediately (columns trimmed for width): + +```text +14:39:54 ACME #6 close=344.77 smaFast=null smaSlow=null ema=null rsi=null macd=null +14:39:58 ACME #10 close=339.16 smaFast=342.59 smaSlow=null ema=null rsi=null macd=null +14:40:03 ACME #15 close=342.19 smaFast=341.35 smaSlow=null ema=null rsi=48.8 macd=null +14:40:08 ACME #20 close=340.14 smaFast=340.18 smaSlow=null ema=325.88 rsi=46.1 macd=null bbUpper=345.51 +14:40:18 ACME #30 close=342.53 smaFast=342.75 smaSlow=341.84 ema=336.75 rsi=50.8 macd=null +14:40:22 ACME #34 close=343.29 smaFast=344.20 smaSlow=341.96 ema=339.30 rsi=51.4 macd=0.8272 macdSignal=-3.2426 +``` + +Each first value lands exactly where the lookback says it should: + +| Indicator | Config | TA-Lib lookback | First bar with a value | +|-----------|--------|----------------:|-----------------------:| +| SMA fast | 10 | 9 | 10 | +| RSI | 14 | 14 | 15 | +| ATR | 14 | 14 | 15 | +| EMA | 20 | 19 | 20 | +| Bollinger | 20 | 19 | 20 | +| SMA slow | 30 | 29 | 30 | +| MACD | 12 / 26 / 9 | 33 | 34 | + +`BarsRequired` on every snapshot is `max(lookback) + 1` = **34** with the defaults, and `BarsInWindow` +tells you how far along you are. `IsWarmedUp` is the convenience: true once every tracked indicator has +a value. + +**Window size versus lookback.** `WindowSize` must be strictly larger than the slowest lookback or that +indicator never fires; the constructor throws if it is not, and startup validation rejects the +configuration before the host serves. But "strictly larger" is the floor, not the target. The default is +**256**, roughly seven times the floor, and the margin is deliberate: exponentially smoothed indicators +have no exact finite lookback. TA-Lib seeds an EMA with a simple average of the first period and then +decays, so an EMA over the last 34 bars is not the same number as an EMA over the last 5 000. At 256 +bars an EMA(20) retains about `(1 - 2/21)^236 β‰ˆ 4e-11` of its seed, far below display precision. A window +of 40 would produce plausible-looking values that quietly disagree with the batch answer. **Choose the +window from the slowest smoothed indicator's decay, not from its nominal period.** + +--- + +## βš™οΈ Configuration + +Everything binds from the `RealTime` section of `appsettings.json` and can be overridden on the command +line with `--RealTime:Key=Value`. + +| Key | Default | Meaning | +|-----|---------|---------| +| `Symbols` | `["ACME","GLOBEX","INITECH"]` | Published symbols. The first is the page's and the client's default | +| `TickIntervalMilliseconds` | `250` | Synthetic tick interval | +| `BarSeconds` | `5` | Bar period. Ticks fold into bars aligned to this | +| `RandomSeed` | `20240613` | Seed of the price walk | +| `WindowSize` | `256` | Closed bars kept per symbol | +| `SubscriberQueueCapacity` | `64` | Items a single subscriber may buffer before the oldest is dropped | +| `Volatility` | `0.0015` | Per-tick log-return standard deviation | +| `MeanReversion` | `0.0025` | Pull back towards the symbol's base price, per tick | +| `Indicators:SmaFastPeriod` | `10` | | +| `Indicators:SmaSlowPeriod` | `30` | | +| `Indicators:EmaPeriod` | `20` | | +| `Indicators:RsiPeriod` | `14` | | +| `Indicators:MacdFastPeriod` | `12` | | +| `Indicators:MacdSlowPeriod` | `26` | | +| `Indicators:MacdSignalPeriod` | `9` | | +| `Indicators:BollingerPeriod` | `20` | | +| `Indicators:BollingerDeviations` | `2.0` | | +| `Indicators:AtrPeriod` | `14` | | +| `Indicators:RsiOverbought` | `70.0` | Signal threshold | +| `Indicators:RsiOversold` | `30.0` | Signal threshold | + +Startup validation (`.Validate(…).ValidateOnStart()`) rejects an empty symbol list, a non-positive tick +interval / bar period / queue capacity, any indicator period below 2 (`TAMath` would return +`RetCode.BadParam` forever, silently nulling that indicator), and a window that is not larger than the +largest lookback. A bad configuration is a loud boot failure, not a quiet stream of nulls. + +> `RealTimeOptions.Symbols` deliberately has **no** code-side default. The configuration binder *appends* +> to a non-empty collection rather than replacing it, so a code default of three symbols plus three in +> configuration binds to six β€” each duplicated, each with its own price walk. Startup validation catches +> the empty case instead. + +--- + +## πŸ› Known library defects visible in this sample + +These are defects in `src/TechnicalAnalysis.Functions`, not in the sample. The sample calls the library +the recommended way and reports what it gets. They are documented here because you will see them in the +output and should not spend an afternoon debugging your own code. + +| Defect | Symptom here | Location | +|--------|--------------|----------| +| **ATR never divides its running average** | `atr` grows by a factor of `period - 1` every bar after the second output; the dashboard's ATR row and the client's ATR column show values like `6.27e+63` | `src/TechnicalAnalysis.Functions/Atr/TAFunc.cs`, main output loop | +| **EMA seeds itself low** | The seed sums `period - 1` values and divides by `period`, so `EMA(20)` reads `325.88` at bar 20 while every close is around `340`. The error decays with the smoothing factor, so it is invisible in steady state but distorts the first few dozen bars after warm-up. Blast radius: everything routed through `TA_INT_EMA` β€” Ema, Macd, MacdExt, MacdFix, Dema, Tema, T3, Apo, Ppo, Trix | `src/TechnicalAnalysis.Functions/TAFunc.cs`, `TA_INT_EMA` seed loop | +| **RSI returns `NaN` on a perfectly flat series** | Not visible in the sample, because the alignment helper rejects non-finite values and reports `null` instead. Real-world trigger: a halted instrument | `src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs` | + +The third one is worth dwelling on: the sample survives it **because** of the `double.IsFinite` guard in +the alignment helper. That guard is not defensive clutter; it is the reason a library defect degrades to +a missing value rather than a `NaN` rendered as a price. + +--- + +## 🏭 Production notes + +### SignalR scale-out and the Redis backplane + +This sample runs one process. `IHubContext.Clients.Group(...)` only reaches connections held by *that* +process, so the moment you put two instances behind a load balancer, a client connected to instance B +stops receiving bars published by instance A. + +```csharp +builder.Services + .AddSignalR() + .AddStackExchangeRedis("redis:6379", options => + options.Configuration.ChannelPrefix = RedisChannel.Literal("talib-rt")); +``` + +What that buys and what it costs: + +* Every group send becomes a Redis publish, fanned out to every instance, which then delivers to its own + local connections. Latency gains a network hop. +* Redis becomes a hard dependency of your *streaming* path. Plan for its failure β€” SignalR will surface + it as delivery errors, not as a graceful degradation. +* The backplane does **not** shard your work. Every instance still receives every message for every + group it has no members of. Past a certain fan-out, per-symbol topics on a real broker beat a + chat-shaped backplane. +* If your indicators are computed per instance (as here), each instance duplicates the CPU. Compute once + in a dedicated worker and publish snapshots onto the bus; do not scale the pipeline by scaling the web + tier. + +WebSocket sticky sessions are still required unless you disable the fallback transports, because the +SignalR negotiate step and the subsequent connection must land on the same instance. + +### Per-connection state cost + +A SignalR WebSocket connection is not free. Budget for, per connection: + +* Two pinned-ish buffers for the transport pipes (default 64 KB each way, tunable via + `HttpConnectionDispatcherOptions.TransportMaxBufferSize` / `ApplicationMaxBufferSize`). +* The connection's own state: id, features, claims, group memberships. +* One OS socket and its kernel buffers. + +Tens of thousands of idle connections per instance is achievable; the failure mode is almost never CPU, +it is memory and file descriptors. Raise `ulimit -n`, and measure with real payload sizes rather than +empty pings. + +In *this* sample there is one more per-subscriber cost: `BoundedFanout` allocates a bounded channel of +`SubscriberQueueCapacity` (default 64) references per subscriber. At 64 Γ— 8 bytes that is negligible per +connection, but the snapshots those references keep alive are not, if a subscriber stalls with a full +queue. That is a deliberate, bounded liability β€” which is the next section. + +### Backpressure: bounded channels, drop oldest + +Every subscriber gets its own bounded channel with `BoundedChannelFullMode.DropOldest`. When a consumer +stops reading β€” a browser tab backgrounded, a console client at a breakpoint, a socket wedged behind a +full TCP window β€” its queue fills and the **oldest** queued item is discarded to make room for the newest. + +This is the right policy for a market feed and the wrong one for an order feed: + +* A late price is worth nothing. A consumer 40 bars behind does not want to replay 40 stale bars; it + wants the current one. +* `BoundedChannelFullMode.Wait` would let the slowest subscriber throttle the producer and therefore + every other subscriber. That is how one wedged client takes down a whole feed. +* An unbounded channel trades the stall for unbounded memory growth β€” the same outage with a longer fuse. + +Dropping is not silent: `Sequence` increases by exactly one per bar per symbol, so a client that cares +can see the gap. The sample's console client counts sequence gaps for exactly this reason. + +#### The push transport needs its own bound + +A bounded channel only protects the paths that *go through* a channel. The SignalR push transport does +not: `IHubContext.Clients.Group(...).SendAsync(...)` fans out as `Task.WhenAll` over per-connection +`WriteAsync` calls, and each of those completes only once that connection's transport pipe has accepted +the frame. A subscriber that stops draining fills its 64 KB pipe and the send stops completing. + +Awaiting that call on the tick loop β€” which is what the obvious implementation does β€” hands one wedged +client the power to stop the whole server: the loop stops consuming ticks, the upstream drop-oldest +`BoundedFanout` discards them, and **every** symbol on **every** transport loses bars, including +clients that are reading perfectly well. The blast radius lasts until ASP.NET Core aborts the wedged +connection, which is governed by `HubOptions.ClientTimeoutInterval` β€” a value deployments routinely +raise, and one a client can keep resetting by sending pings while never reading. + +`IndicatorPipeline` therefore gives the push path the same treatment as the pull path, twice over: + +* The tick loop never awaits the hub. It hands the snapshot to a bounded drop-oldest queue with + `TryWrite`, which cannot block and cannot throw, and moves on to the next tick. +* A separate pump drains that queue and does the group send with a 250 ms deadline. On expiry the bar + is abandoned for that group and `pushesDropped` on `/health` goes up. + +The guarantee that comes out of this is precise, and it is worth stating precisely: **a wedged +subscriber costs its own group the bars it did not read, and nothing else.** Tick consumption, bar +aggregation, the last-snapshot cache and both pull transports carry on at full rate. It is not a +guarantee that every subscriber receives every bar β€” that was never on offer here, and the drop-oldest +rationale above explains why it should not be. + +**Anything that must not be lost β€” fills, cancels, risk limits β€” belongs on a different, acknowledged +channel.** Do not put them on a drop-oldest fanout because it happened to be there. + +### Cancellation and disconnect handling + +Three lessons from this sample worth stealing: + +1. **Subscribe before you await.** `IndicatorPipeline` subscribes to the feed before its first `await`, + and `StreamSnapshots` subscribes before yielding the replayed snapshot. Otherwise a bar can slip + between the replay and the live stream and be lost forever. +2. **A one-way socket still needs a receive loop.** Without one, the close handshake never completes and + you accumulate connections that are alive to you and dead to the peer. Use `CloseOutputAsync` (send + half only) when another loop owns the read side; `CloseAsync` issues its own receive and will collide + with it. +3. **Honour the token you are given.** SignalR's `[EnumeratorCancellation] CancellationToken` fires when + the client disposes its stream, when the connection drops, *and* when the host shuts down. Honouring + it is what makes the *streaming* subscription leak-free. Verified: after every streaming and raw + WebSocket client disconnected, `/health` reported `"snapshotSubscribers":0`. + + That check says nothing about the **push** transport, which is the one most clients use. A group + membership is not a channel, so it cannot show up in `snapshotSubscribers` however leaky it is; + `IndicatorHub` therefore tracks joins and leaves explicitly and reconciles them in + `OnDisconnectedAsync`, and `/health` reports them separately as `groupSubscribers`. Watch that + field, not the other one, when you are looking for a leaked push subscription. + +`FanoutSubscription.Dispose` is idempotent (guarded with `Interlocked`) and completes the channel so a +pending read finishes β€” so the usual `using` / `finally` / disconnect-handler triple call is harmless. + +On the client side, `WithAutomaticReconnect()` is not enough on its own: **group membership does not +survive a new connection id**, so you must re-`Subscribe` in the `Reconnected` handler. The sample's +console client does this. + +### When to move to a true incremental indicator + +The sample recomputes the whole window on every bar. That is O(window) per indicator per bar β€” a few +microseconds for a 256-bar window and seven indicators, which is nothing next to one bar per second and +nothing next to the network hop that follows. + +The alternative is genuine incremental state: a running sum for SMA, the previous value and smoothing +factor for EMA, Wilder's averages for RSI and ATR, advanced one bar at a time. O(1) per bar, and it is +what a production feed handler does. + +**The trade is not performance versus elegance, it is performance versus agreement.** The moment your EMA +seeding or your RSI smoothing differs from the library's by an epsilon, your live values and your +backtest values quietly disagree, and that difference is extremely hard to find later. Window recompute +guarantees that the number a client sees is produced by the same library, on the same code path, as the +number a backtest sees. + +Move to incremental when, and only when: + +* the window recompute actually shows up in a profile β€” realistically at high bar rates (sub-second + bars), or thousands of symbols per process, or a very large window; +* you can pin the incremental implementation with tests that assert equality against the batch values + over a long series, including the warm-up region; +* you have decided what to do about the seed: an incremental EMA started today is not the same number as + a batch EMA over five years of history, and you must pick which one is "correct" for your product. + +A sensible middle path is to keep the window recompute and shrink the window to the smallest size whose +divergence from a long-history batch computation is below your display precision β€” which is the same +decay calculation as in [Warm-up and null semantics](#-warm-up-and-null-semantics), run in reverse. + +### Things this sample deliberately does not do + +State them out loud so nobody ships them by accident: + +* **No authentication or authorisation.** The hub and the socket are open. Add `[Authorize]` and a real + identity before this faces anything. +* **No persistence.** Restarting the server restarts the price walk and loses every bar. +* **No historical backfill.** A new subscriber gets the last snapshot and then live bars; there is no + "give me the last 200 bars" call. Real dashboards need one. +* **No rate limiting, no connection quotas, no origin checks** on the WebSocket endpoint. +* **No TLS.** `http://localhost:5199`. Terminate TLS in front of it and use `wss://`. +* **No multi-instrument correlation, no order routing, no risk.** It computes indicators and publishes + them. The `Signal` enum is explicitly labelled as a demonstration of combining indicator outputs, not + as something to trade. +* **No automated tests.** `RollingIndicatorEngine`'s alignment logic and `BarAggregator`'s bucketing are + the two pieces most worth pinning if you adapt this. + +--- + +## Related + +* [`samples/TechnicalAnalysis.Samples.RealTime/README.md`](../../samples/TechnicalAnalysis.Samples.RealTime/README.md) + β€” project-local README with the file-by-file layout. +* [Getting started](getting-started.md) β€” `RetCode`, `BegIdx`/`NBElement` and the alignment rule in + detail. +* [Backtesting](backtesting.md) β€” the same alignment rule, applied to history instead of a live feed. +* [TradingView integration](tradingview-integration.md) β€” repainting, realtime bars and the + compute-on-closed-bars pattern. diff --git a/docs/guides/tradingview-integration.md b/docs/guides/tradingview-integration.md new file mode 100644 index 00000000..0cc7f9d9 --- /dev/null +++ b/docs/guides/tradingview-integration.md @@ -0,0 +1,829 @@ +# TradingView integration + +TaLibStandard and TradingView meet in three places: + +1. **You are porting a Pine Script strategy to C#** and need to know which `TAMath` call replaces + which `ta.*` function β€” and where the two will legitimately disagree. +2. **You want a TradingView chart to display series that TaLibStandard computed**, which means + implementing the UDF (Universal Data Feed) REST contract, or using Lightweight Charts. +3. **You want TradingView alerts to drive a .NET strategy**, which means receiving webhooks safely. + +This guide covers all three. Read [Getting started](getting-started.md) first β€” in particular +Β§3 on `BegIdx` / `NBElement`, because every bar-index mistake in this guide's territory shows up as +a signal that fires one bar early or late. + +--- + +## 0. Known library defects + +**Read this before you reconcile anything.** Three defects in `src/TechnicalAnalysis.Functions` make +certain outputs impossible to match against a chart at *any* tolerance. If you are chasing one of +them, no amount of data provenance work will help. They are defects in the port, not differences in +convention, and none of them is Pine's fault. + +| Defect | What you see | Location | +|---|---|---| +| **`Atr` never divides its running average** | `TAMath.Atr` grows by a factor of `period - 1` on every bar after the second output, reaching `+∞` within a few hundred bars. On a 1 500-bar series it reads `2.32` at bar 14, `29.97` at bar 16, `4.4e+110` at bar 114 and `+∞` from bar 300 onwards | `src/TechnicalAnalysis.Functions/Atr/TAFunc.cs`, main output loop | +| **`TA_INT_EMA` seeds itself low** | The seed sums `period - 1` values and divides by `period`, then smooths once more. The error decays with the smoothing factor, so it distorts the bars just after warm-up rather than the steady state. Blast radius: `Ema`, `Macd`, `MacdExt`, `MacdFix`, `Dema`, `Tema`, `T3`, `Apo`, `Ppo`, `Trix` | `src/TechnicalAnalysis.Functions/TAFunc.cs`, `TA_INT_EMA` seed loop | +| **`Rsi` returns `NaN` on a perfectly flat series** | `RetCode.Success`, a non-zero `NBElement`, and every element `NaN` β€” no zero guard on `prevGain + prevLoss`. Real-world trigger: a halted instrument, or any window with no price change | `src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs` | + +What this means for the rest of this guide: + +- **`Atr`** β€” do not compare it to `ta.atr` at all. If you need ATR today, compute Wilder smoothing + yourself over `TAMath.TrueRange`; the `Rma` helper in [Β§2.1](#21-wilder-smoothing-tarma) is + exactly that recurrence and it is a dozen lines. +- **The EMA family** β€” expect a discrepancy against Pine that is *larger* than the seeding + convention of [Β§2.2](#22-seeding-and-warm-up) explains, and that shrinks as you move away from the + start of the series. Raising the unstable period ([Β§2.2](#22-seeding-and-warm-up)) discards the + contaminated bars and is the practical mitigation. +- **`Rsi`** β€” guard your consumer with `double.IsFinite` before charting or comparing. `TAMath.Rsi` + is otherwise structurally exact against `ta.rsi` (see [Β§2.1](#21-wilder-smoothing-tarma)). + +`Natr`, `Adx`, `Adxr`, `Dx`, `Cmo`, `PlusDM` and `MinusDM` were checked against the same inputs and +are **not** affected β€” they are reconcilable in the normal way. + +The same list appears in +[Backtesting β†’ Limitations](backtesting.md#-limitations--read-before-believing-any-number) and in +[Real-time streaming β†’ Known library defects](real-time-streaming.md#-known-library-defects-visible-in-this-sample), +which show what each defect does to a running strategy. + +--- + +## 1. Pine Script `ta.*` β†’ TaLibStandard + +Signatures below are the `double[]` overloads. `last` means `close.Length - 1`. Every call also has +a `float[]` overload. See the [indicator reference](../indicators/README.md) for the full parameter +lists and defaults. + +### Moving averages and overlays + +| Pine Script | TaLibStandard | Notes | +| --- | --- | --- | +| `ta.sma(src, len)` | `TAMath.Sma(0, last, src, timePeriod: len)` | Exact structural match. | +| `ta.ema(src, len)` | `TAMath.Ema(0, last, src, timePeriod: len)` | Different seeding, and TaLibStandard's seed is also wrong β€” see [Β§0](#0-known-library-defects) and [Β§2.2](#22-seeding-and-warm-up). | +| `ta.wma(src, len)` | `TAMath.Wma(0, last, src, timePeriod: len)` | Exact structural match. | +| `ta.rma(src, len)` | *no direct entry point* | Wilder smoothing. Used internally by `Rsi`, `Atr`, `Adx`, `Dx`, `Cmo`, `Natr`, `PlusDM`, `MinusDM`. Roll your own if you need it standalone β€” see [Β§2.1](#21-wilder-smoothing-tarma). | +| *(no Pine builtin)* | `TAMath.Dema`, `TAMath.Tema`, `TAMath.Trima`, `TAMath.Kama`, `TAMath.T3`, `TAMath.Mama` | Pine users normally hand-roll these; TaLibStandard ships them. | +| `ta.vwma(src, len)` | *no equivalent* | Compose: `TAMath.Sma` of `src * volume` divided by `TAMath.Sma` of `volume`. | +| `ta.hma`, `ta.alma`, `ta.swma`, `ta.linreg`-based bands | *no equivalent* | `ta.hma` is composable from three `TAMath.Wma` calls. | +| `ta.bb(src, len, mult)` | `TAMath.BollingerBands(0, last, src, timePeriod: len, nbDevUp: mult, nbDevDn: mult, maType: MAType.Sma)` | **Tuple order differs**: Pine returns `[middle, upper, lower]`; the result exposes `RealUpperBand`, `RealMiddleBand`, `RealLowerBand`. | +| `ta.bbw(src, len, mult)` | *no equivalent* | `(RealUpperBand[k] - RealLowerBand[k]) / RealMiddleBand[k]`. | +| `ta.kc` (Keltner) | *no equivalent* | Compose from `TAMath.Ema` + `TAMath.Atr`. | +| `ta.sar(start, inc, max)` | `TAMath.Sar(0, last, high, low, acceleration: start, maximum: max)` | Pine exposes `start` **and** `increment` separately; TA-Lib's `acceleration` is both. Use `TAMath.SarExt` for independent long/short acceleration control. | +| `ta.supertrend(factor, atrPeriod)` | *no equivalent* | Compose from `TAMath.Atr`. | +| `ta.vwap` | *no equivalent* | See [Β§2.7](#27-vwap-has-no-ta-lib-equivalent). | +| *(no Pine builtin)* | `TAMath.ZigZag(0, last, high, low, deviation: 5.0)` | TaLibStandard extension, not part of TA-Lib. | + +### Momentum and oscillators + +| Pine Script | TaLibStandard | Notes | +| --- | --- | --- | +| `ta.rsi(src, len)` | `TAMath.Rsi(0, last, src, timePeriod: len)` | Same recurrence and same seeding β€” the closest thing to exact parity in this table. | +| `ta.macd(src, fast, slow, signal)` | `TAMath.Macd(0, last, src, optInFastPeriod: fast, optInSlowPeriod: slow, optInSignalPeriod: signal)` | Returns `MacdValue`, `MacdSignal`, `MacdHist`, matching Pine's `[macdLine, signalLine, histLine]`. Inherits EMA seeding differences. | +| `ta.stoch(src, high, low, len)` | `TAMath.StochF(0, last, high, low, close, fastKPeriod: len, fastDPeriod: 3, fastDMAType: MAType.Sma).FastK` | `ta.stoch` is **raw %K, unsmoothed**. Use `StochF.FastK`, not `TAMath.Stoch`. | +| TradingView "Stochastic" indicator (`%K` smoothing 3, `%D` 3) | `TAMath.Stoch(0, last, high, low, close, fastKPeriod: 14, slowKPeriod: 3, slowKMAType: MAType.Sma, slowDPeriod: 3, slowDMAType: MAType.Sma)` | `SlowK`, `SlowD`. | +| `ta.stoch` on `ta.rsi` (Stoch RSI) | `TAMath.StochRsi(0, last, src, timePeriod: 14, fastKPeriod: 5, fastDPeriod: 3, fastDMAType: MAType.Sma)` | `FastK`, `FastD`. | +| `ta.cci(src, len)` | `TAMath.Cci(0, last, high, low, close, timePeriod: len)` | Pine takes an arbitrary `src`; TA-Lib always uses typical price `(H+L+C)/3` internally. Compare against `ta.cci(hlc3, len)`. | +| `ta.mfi(src, len)` | `TAMath.Mfi(0, last, high, low, close, volume, timePeriod: len)` | Same typical-price caveat; compare against `ta.mfi(hlc3, len)`. | +| `ta.dmi(diLen, adxSmoothing)` | `TAMath.PlusDI(...)`, `TAMath.MinusDI(...)`, `TAMath.Adx(0, last, high, low, close, timePeriod: len)` | TA-Lib uses **one** period for both DI and ADX smoothing. `ta.dmi(14, 14)` is the comparable call. | +| `ta.mom(src, len)` / `ta.change(src, len)` | `TAMath.Mom(0, last, src, timePeriod: len)` | `ta.change(src)` with no length is `TAMath.Mom(..., timePeriod: 1)`. | +| `ta.roc(src, len)` | `TAMath.Roc(0, last, src, timePeriod: len)` | Percent change. `TAMath.RocP`, `TAMath.RocR`, `TAMath.RocR100` are the other three TA-Lib conventions. | +| `ta.cmo(src, len)` | `TAMath.Cmo(0, last, src, timePeriod: len)` | | +| `ta.wpr(len)` | `TAMath.WillR(0, last, high, low, close, timePeriod: len)` | Williams %R. | +| `ta.tsi(src, short, long)` | *no equivalent* | Compose from nested `TAMath.Ema`. | +| *(no Pine builtin)* | `TAMath.Apo`, `TAMath.Ppo`, `TAMath.UltOsc`, `TAMath.Bop`, `TAMath.Aroon`, `TAMath.AroonOsc`, `TAMath.Adxr`, `TAMath.Trix`, `TAMath.MacdExt`, `TAMath.MacdFix` | | + +### Volatility and volume + +| Pine Script | TaLibStandard | Notes | +| --- | --- | --- | +| `ta.atr(len)` | `TAMath.Atr(0, last, high, low, close, timePeriod: len)` | **Do not reconcile this one.** `TAMath.Atr` diverges to `+∞`; see [Β§0](#0-known-library-defects). Compose from `TAMath.TrueRange` instead. | +| `ta.tr(handle_na)` | `TAMath.TrueRange(0, last, high, low, close)` | `TrueRange` has lookback 1 (`BegIdx == 1`), so there is no bar-0 value. `ta.tr(true)` substitutes `high - low` on bar 0; `ta.tr(false)` returns `na`. TaLibStandard behaves like `ta.tr(false)`. | +| *(no Pine builtin)* | `TAMath.Natr(0, last, high, low, close, timePeriod: len)` | ATR normalised as a percentage of close. | +| `ta.obv` | `TAMath.Obv(0, last, close, volume)` | `ObvLookback()` is `0`, so `BegIdx == startIdx`. Absolute level is arbitrary in both (only the slope matters). | +| `ta.accdist` | `TAMath.Ad(0, last, high, low, close, volume)` | Chaikin Accumulation/Distribution line. | +| *(no Pine builtin)* | `TAMath.AdOsc(0, last, high, low, close, volume, fastPeriod: 3, slowPeriod: 10)` | Chaikin A/D Oscillator. | +| `ta.pvt`, `ta.nvi`, `ta.pvi`, `ta.wad` | *no equivalent* | | + +### Statistics and array helpers + +| Pine Script | TaLibStandard | Notes | +| --- | --- | --- | +| `ta.stdev(src, len)` | `TAMath.StdDev(0, last, src, timePeriod: len, nbDev: 1.0)` | Both are **population** (biased) standard deviation, matching Pine's `biased = true` default. | +| `ta.variance(src, len)` | `TAMath.Variance(0, last, src, timePeriod: len, nbDev: 1.0)` | Same. | +| `ta.correlation(a, b, len)` | `TAMath.Correl(0, last, a, b, timePeriod: len)` | | +| `ta.linreg(src, len, offset)` | `TAMath.LinearReg(0, last, src, timePeriod: len)` | Only `offset = 0`. `TAMath.LinearRegSlope`, `LinearRegAngle`, `LinearRegIntercept` expose the components; `TAMath.Tsf` is the one-bar-ahead forecast. | +| `ta.highest(src, len)` / `ta.lowest(src, len)` | `TAMath.Max(0, last, src, timePeriod: len)` / `TAMath.Min(0, last, src, timePeriod: len)` | | +| `ta.highestbars(...)` / `ta.lowestbars(...)` | `TAMath.MaxIndex(...)` / `TAMath.MinIndex(...)` | These return **absolute input indices** in `Integers`; Pine returns a negative bar offset. Convert with `Integers[k] - (BegIdx + k)`. | +| `ta.sum(src, len)` | `TAMath.Sum(0, last, src, timePeriod: len)` | Rolling sum. `ta.cum` (running total from bar 0) has no equivalent. | +| `ta.percentrank`, `ta.percentile_*`, `ta.median`, `ta.mode`, `ta.dev`, `ta.range` | *no equivalent* | | +| `ta.crossover(a, b)` / `ta.crossunder(a, b)` | *no equivalent* | Two-line C#; see the snippet below. | +| `ta.barssince`, `ta.valuewhen`, `ta.pivothigh`, `ta.pivotlow` | *no equivalent* | State machines, not indicators. | + +```csharp +// ta.crossover / ta.crossunder over two aligned (NaN-padded) series. +static bool CrossedOver(double[] a, double[] b, int i) + => i > 0 && !double.IsNaN(a[i - 1]) && !double.IsNaN(b[i - 1]) + && a[i - 1] <= b[i - 1] && a[i] > b[i]; + +static bool CrossedUnder(double[] a, double[] b, int i) + => i > 0 && !double.IsNaN(a[i - 1]) && !double.IsNaN(b[i - 1]) + && a[i - 1] >= b[i - 1] && a[i] < b[i]; +``` + +### Math transforms and operators + +Pine exposes these as `math.*` (`math.abs`, `math.log`, `math.sqrt`, …) operating on a single value +per bar. TaLibStandard's equivalents operate on the whole array in one call: `TAMath.Acos`, `Asin`, +`Atan`, `Ceil`, `Cos`, `Cosh`, `Exp`, `Floor`, `Ln`, `Log10`, `Sin`, `Sinh`, `Sqrt`, `Tan`, `Tanh`, +plus the element-wise operators `Add`, `Sub`, `Mult`, `Div`. + +Those have lookback 0, so `BegIdx == startIdx` and `NBElement == endIdx - startIdx + 1`. The rolling +operators `Max`, `Min`, `MinMax`, `MaxIndex`, `MinIndex`, `MinMaxIndex` and `Sum` take a +`timePeriod` and therefore have lookback `timePeriod - 1` like any other windowed indicator. + +### Candlestick patterns + +Pine has no built-in candlestick-pattern functions (TradingView ships them as a closed built-in +script). TaLibStandard exposes all 61 TA-Lib patterns through `TACandle`, generic over +`IFloatingPoint`: + +```csharp +CandleIndicatorResult engulfing = TACandle.CdlEngulfing(0, last, open, high, low, close); +// Integers[k] is bar (BegIdx + k): 0 = none, +100 = bullish, -100 = bearish +``` + +See the [Pattern Recognition section of the indicator reference](../indicators/README.md#pattern-recognition). + +--- + +## 2. Parity caveats + +**Do not expect bit-for-bit equality with a TradingView chart.** Some differences are algorithmic, +some are data-provenance, and some are a property of how TradingView evaluates scripts in realtime. +Know which is which before you spend a day chasing a 0.3% discrepancy. + +### 2.1 Wilder smoothing (`ta.rma`) + +Pine's `ta.rma(src, len)` is the recurrence `rma = (src + (len - 1) * rma[1]) / len`, seeded with +`ta.sma(src, len)`. TaLibStandard's RSI implementation +(`src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs`) uses exactly that recurrence +(`prev *= len - 1; prev += delta; prev /= len`) with exactly that seed β€” the simple average of the +first `len` gains and losses. The same smoothing is *intended* to drive `Atr`, `Adx`, `Dx`, `Cmo`, +`Natr`, `PlusDM` and `MinusDM`. + +So `ta.rsi` and `TAMath.Rsi` agree structurally; what is left between them is history dependence +(Β§2.3) and the data itself (Β§2.5–2.8). `TAMath.Adx` and `TAMath.Natr` were checked and are fine too. + +**`ta.atr` and `TAMath.Atr` do not agree, and cannot be reconciled at any tolerance.** `TAFunc.Atr`'s +main output loop divides the *emitted* value by `len` but never the accumulator, so the running +average is multiplied by `len - 1` on every bar. Measured in this repository on a series whose true +range is exactly `2.0` every bar: + +```text +TAMath.Atr(0, 29, high, low, close, timePeriod: 14) + BegIdx=14 NBElement=16 + Real[0..7] = 2, 2, 26.142857, 340, 4420.142857, 57462, 747006.142857, 9711080 +``` + +Each value after the second is exactly 13Γ— (`len - 1`) the previous one. See +[Β§0](#0-known-library-defects). + +Measured, for the reference series +`44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00, …`: + +```text +TAMath.Rsi(0, 29, px, timePeriod: 14) + BegIdx=14 NBElement=16 + Real[0..4] = 70.4641, 66.2496, 66.4809, 69.3469, 66.2947 +``` + +`Real[0]` is bar 14, i.e. the fifteenth close. Pine's `ta.rsi(close, 14)` on the same 30 closes +produces its first non-`na` value on the same bar. + +If you need `ta.rma` standalone, it is a dozen lines: + +```csharp +static double[] Rma(double[] src, int length) +{ + double[] result = new double[src.Length]; + Array.Fill(result, double.NaN); + + double seed = 0.0; + for (int i = 0; i < length; i++) + { + seed += src[i]; + } + + result[length - 1] = seed / length; + for (int i = length; i < src.Length; i++) + { + result[i] = ((result[i - 1] * (length - 1)) + src[i]) / length; + } + + return result; +} +``` + +### 2.2 Seeding and warm-up + +Two distinct issues: + +**Seeding.** There are two separate things going on here, and only one of them is a convention. + +*The convention.* Upstream TA-Lib seeds `prevMA` with the simple average of the first `timePeriod` +inputs. Pine documents `ta.ema` with a reference implementation that seeds from the first source +value. **A seeding difference produces a transient that is multiplied by `1 - 2/(len + 1)` on every +bar and never reaches exactly zero.** For `len = 26` that factor is `0.926` β€” a 7.4% reduction per +bar β€” so after 100 bars the residual is ~0.05% of the initial gap and after 300 bars it is +numerically invisible. + +*The defect.* TaLibStandard does not implement the upstream seed. `TA_INT_EMA` +(`src/TechnicalAnalysis.Functions/TAFunc.cs`) accumulates only `timePeriod - 1` inputs and then +divides by `timePeriod`, and the loop that follows applies one extra smoothing step. The seed is +therefore *(sum of the first `timePeriod - 1` inputs) / `timePeriod`*, smoothed once with the +`timePeriod`-th input β€” not the simple average the C library uses. Measured here: + +```text +TAMath.Ema(0, 7, [100, 102, 101, 105, 107, 106, 110, 111], timePeriod: 3) + BegIdx=2 Real[0] = 84.166667 (upstream TA-Lib: 101, the SMA of the first three closes) + +TAMath.Ema(0, 9, [10, 10, 10, 10, 10, 10, 10, 10, 10, 10], timePeriod: 5) + Real[0] = 8.666667, Real[1] = 9.111111 (both should be exactly 10) +``` + +A linear ramp such as `[10, 20, 30, …]` happens to return the right answer by coincidence, which is +why this survives a casual check. So **the gap against Pine is larger than a seeding convention +would explain**, and the residual it leaves behind decays at the same rate but from a bigger start. +The affected entry points are `Ema`, `Macd`, `MacdExt`, `MacdFix`, `Dema`, `Tema`, `T3`, `Apo`, +`Ppo` and `Trix` β€” see [Β§0](#0-known-library-defects). + +**Warm-up representation.** Pine emits `na` for bars it cannot compute and keeps the series +bar-aligned. TaLibStandard emits a *shorter* array and tells you where it starts via `BegIdx`. When +you diff a TaLibStandard series against a TradingView CSV export, align on **timestamps**, never on +array position. + +**Mitigation.** Raise the unstable period so the transient is discarded rather than plotted: + +```csharp +using TechnicalAnalysis; +using TechnicalAnalysis.Common; + +TACore.Globals.UnstablePeriod[FuncUnstId.Ema] = 200; +TACore.Globals.UnstablePeriod[FuncUnstId.Rsi] = 200; +TACore.Globals.UnstablePeriod[FuncUnstId.Adx] = 200; +TACore.Globals.UnstablePeriod[FuncUnstId.Atr] = 200; +``` + +This costs you 200 bars of output but makes results independent of where your history starts. Set it +once at start-up; it is process-wide mutable state. + +### 2.3 Recursive indicators depend on how far back your history goes + +TradingView computes a chart's indicators over *all* the history the data provider has for that +symbol and resolution. If your database starts in 2020 and the chart starts in 2012, your RSI(14), +ATR(14), ADX(14), EMA(200) and every Hilbert Transform output will differ from the chart's β€” forever, +by an amount that shrinks the further you get from your start date but is never zero. + +This is not a bug in either implementation. It is the definition of an infinite-impulse-response +filter. Fetch more history, or raise the unstable period, or accept the difference. + +### 2.4 Realtime bars and repainting + +On the realtime (rightmost, still-forming) bar, TradingView re-evaluates the script on every tick. +An indicator value you read at 10:31:07 is not the value the same bar will have at close. + +- A signal computed on an unconfirmed bar can appear and disappear. That is *repainting*. +- Pine scripts guard with `barstate.isconfirmed`; a strategy alert configured + "Once Per Bar Close" is the equivalent at the alert level. +- TaLibStandard has no notion of bar state. **You** decide what goes into the array. If you append + the forming bar's current close, every recursive indicator's tail repaints on the next tick. + +The safe pattern is: compute on closed bars only, and treat the forming bar separately. + +```csharp +// bars[^1] is still forming; exclude it from the signal calculation. +int lastClosed = bars.Count - 2; +RsiResult rsi = TAMath.Rsi(0, lastClosed, closes, timePeriod: 14); +``` + +Two further asymmetries worth knowing: TradingView's historical bars are evaluated once per bar +(intrabar order of events inside a historical bar is unknown to the script), whereas realtime bars +are evaluated many times. Strategies that behave differently on historical vs realtime data are +usually falling into this gap, not into a TaLibStandard bug. + +### 2.5 Session, timezone and bar-boundary alignment + +A "daily bar" is not a universal concept. + +- TradingView aggregates by the **exchange's** session and timezone. Your database probably + aggregates by UTC midnight. +- Regular-hours vs extended-hours: TradingView's `Extended hours` toggle changes which trades form + the bar. Two different OHLC series, two different indicator series. +- Intraday bars align to the session open, not to the hour. A 4-hour chart of a market that opens at + 09:30 has bars at 09:30/13:30/…, not 08:00/12:00/…. +- Holiday calendars and half-days shift bar counts, which shifts every `length`-based window. + +Before comparing numbers, confirm both sides produce **identical OHLCV** for the overlapping range. +If the inputs differ, the indicators must differ, and nothing about the indicator code is at fault. + +### 2.6 Dividend and split adjustment + +TradingView's `Adjust data for dividends` setting (and its split adjustment, which is always on for +most feeds) rewrites historical prices. A back-adjusted series and a raw series have different +returns, therefore different RSI, different ATR, different everything. + +Decide explicitly which convention you want, apply it on both sides, and record the choice. Also +note that back-adjustment is *retroactive*: a series recomputed after a corporate action is not the +series you computed yesterday. + +### 2.7 VWAP has no TA-Lib equivalent + +There is no `TAMath.Vwap`. TA-Lib never had one, and TaLibStandard is a faithful port. + +More importantly, VWAP is **anchored**, not rolling: Pine's `ta.vwap` resets at the start of each +session (or at a user-chosen anchor). A rolling `Sum(price * volume) / Sum(volume)` is a different +indicator with a similar name. If you need Pine-compatible VWAP, implement the session reset +yourself: + +```csharp +static double[] SessionVwap(DateTime[] barTimes, double[] typicalPrice, double[] volume, TimeZoneInfo exchangeTz) +{ + ArgumentNullException.ThrowIfNull(exchangeTz); + + double[] vwap = new double[typicalPrice.Length]; + double cumulativePv = 0.0; + double cumulativeVolume = 0.0; + DateOnly currentSession = default; + + for (int i = 0; i < typicalPrice.Length; i++) + { + DateTime local = TimeZoneInfo.ConvertTimeFromUtc(barTimes[i], exchangeTz); + DateOnly session = DateOnly.FromDateTime(local); + if (session != currentSession) + { + currentSession = session; + cumulativePv = 0.0; + cumulativeVolume = 0.0; + } + + cumulativePv += typicalPrice[i] * volume[i]; + cumulativeVolume += volume[i]; + vwap[i] = cumulativeVolume > 0.0 ? cumulativePv / cumulativeVolume : double.NaN; + } + + return vwap; +} +``` + +Feed it `TAMath.TypPrice(0, last, high, low, close).Real` for the `(H+L+C)/3` convention that +TradingView uses by default. + +### 2.8 Parameter-shape mismatches + +Collected from Β§1, because these produce *large* differences that look like bugs: + +| Symptom | Cause | +| --- | --- | +| Stochastic is far noisier than the chart | `ta.stoch` is raw %K. Use `StochF.FastK`, or `TAMath.Stoch` for the smoothed version. | +| Bollinger bands look inverted | Pine's `ta.bb` tuple is `[middle, upper, lower]`. | +| CCI / MFI drift from the chart | Pine takes an arbitrary `src`; TA-Lib hard-codes typical price. Compare against `ta.cci(hlc3, len)`. | +| ADX differs | Pine's `ta.dmi` has separate `diLength` and `adxSmoothing`; TA-Lib uses one period for both. | +| Parabolic SAR differs | Pine has `start`, `increment` **and** `maximum`; `TAMath.Sar` has `acceleration` (= start = increment) and `maximum`. Use `TAMath.SarExt` for independent control. | +| `highestbars` sign is wrong | `MaxIndex` returns an absolute index, Pine a negative offset. | +| True range has no bar-0 value | `TAMath.TrueRange` behaves like `ta.tr(false)`. | + +### 2.9 A parity checklist + +1. Export the chart's data and indicator via TradingView's *Export chart data…* (CSV). +2. Load the CSV's OHLCV into your test and assert it matches your own bars **exactly** on the + overlapping timestamps. Fix the data before looking at the indicator. +3. Compute the indicator with TaLibStandard over the **same** bar array. +4. Join on timestamp, not on index β€” remember `BegIdx`. +5. Compare with a tolerance that reflects the mechanism: `1e-9` relative for `Sma`/`Wma`/`StdDev` + (non-recursive), `1e-4` relative for `Rsi`/`Adx`/`Natr` after a few hundred bars, and looser near + the start of the series. +6. If a recursive indicator is off by more than that, extend history or raise its unstable period + before suspecting the port. +7. **Skip `Atr` and the EMA family entirely.** No tolerance reconciles them today; see + [Β§0](#0-known-library-defects). Reconciling anything else first is the fastest way to find out + whether your data is the problem. + +--- + +## 3. Serving TaLibStandard series to a chart + +### 3.1 Licensing, up front + +| Product | Licence | What it costs you | +| --- | --- | --- | +| **Lightweight Charts** | Apache-2.0, public npm package | Free for commercial use. Attribution notice required. No TradingView agreement. | +| **Advanced Charts / Charting Library** | Proprietary; free of charge but requires signing TradingView's licence agreement and being granted repository access | You must apply to TradingView. Do **not** vendor it into a public repo. | +| **UDF protocol itself** | A documented HTTP contract | Nothing. You can implement and publish a UDF server freely; it is only *useful* to a Charting Library client. | + +The sections below implement the UDF contract (useful if you have a Charting Library licence) and +show the Lightweight Charts path (useful to everyone). + +### 3.2 The UDF REST contract + +The Charting Library's UDF adapter calls a small set of endpoints. The three that matter: + +#### `GET /config` + +Advertises capabilities. Returned once at start-up. + +```json +{ + "supported_resolutions": ["1", "5", "15", "60", "D"], + "supports_group_request": false, + "supports_marks": false, + "supports_search": true, + "supports_timescale_marks": false, + "supports_time": true +} +``` + +#### `GET /symbols?symbol=` + +Resolves one symbol. + +```json +{ + "name": "SYNTH", + "ticker": "SYNTH", + "description": "SYNTH (synthetic)", + "type": "crypto", + "session": "24x7", + "timezone": "Etc/UTC", + "exchange": "TALIB", + "listed_exchange": "TALIB", + "minmov": 1, + "pricescale": 100, + "has_intraday": true, + "supported_resolutions": ["1", "5", "15", "60", "D"], + "volume_precision": 0, + "data_status": "streaming" +} +``` + +`session` and `timezone` are the fields that decide bar-boundary alignment on the chart β€” get them +wrong and every parity comparison from Β§2.5 fails. + +#### `GET /history?symbol=&resolution=&from=&to=` + +Column-oriented OHLCV. `from`/`to` are **UNIX seconds**, and `t` is a UNIX-seconds array. + +```json +{ + "s": "ok", + "t": [1704153600, 1704240000, 1704326400], + "o": [100.0, 102.0, 101.0], + "h": [101.5, 103.0, 102.2], + "l": [ 99.2, 101.1, 100.4], + "c": [102.0, 101.0, 105.0], + "v": [1200, 900, 1500] +} +``` + +When the requested window is empty, the shape changes β€” this is not an error: + +```json +{ "s": "no_data", "nextTime": 1704412800 } +``` + +`nextTime` is the timestamp of the closest bar **after** `to`, so the client can jump instead of +paging blindly. On a genuine failure return `{ "s": "error", "errmsg": "..." }`. + +#### Minimal API implementation + +```csharp +using System.Text.Json.Serialization; +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.Services.AddSingleton(); +WebApplication app = builder.Build(); + +app.MapGet("/udf/config", () => Results.Json(new UdfConfig( + Supported_Resolutions: ["1", "5", "15", "60", "D"], + Supports_Group_Request: false, + Supports_Marks: false, + Supports_Search: true, + Supports_Timescale_Marks: false, + Supports_Time: true))); + +app.MapGet("/udf/symbols", (string symbol) => Results.Json(new UdfSymbolInfo( + Name: symbol, + Ticker: symbol, + Description: $"{symbol} (synthetic)", + Type: "crypto", + Session: "24x7", + Timezone: "Etc/UTC", + Exchange: "TALIB", + Listed_Exchange: "TALIB", + Minmov: 1, + Pricescale: 100, + Has_Intraday: true, + Supported_Resolutions: ["1", "5", "15", "60", "D"], + Volume_Precision: 0, + Data_Status: "streaming"))); + +app.MapGet("/udf/history", (string symbol, string resolution, long from, long to, IBarStore store) => +{ + IReadOnlyList bars = store.GetBars(symbol, resolution, from, to); + if (bars.Count == 0) + { + return Results.Json(new UdfHistoryNoData("no_data", store.NextBarTime(symbol, resolution, to))); + } + + return Results.Json(new UdfHistory( + S: "ok", + T: bars.Select(b => b.Time).ToArray(), + O: bars.Select(b => b.Open).ToArray(), + H: bars.Select(b => b.High).ToArray(), + L: bars.Select(b => b.Low).ToArray(), + C: bars.Select(b => b.Close).ToArray(), + V: bars.Select(b => b.Volume).ToArray())); +}); + +app.Run(); + +internal sealed record Bar(long Time, double Open, double High, double Low, double Close, double Volume); + +internal sealed record UdfConfig( + [property: JsonPropertyName("supported_resolutions")] string[] Supported_Resolutions, + [property: JsonPropertyName("supports_group_request")] bool Supports_Group_Request, + [property: JsonPropertyName("supports_marks")] bool Supports_Marks, + [property: JsonPropertyName("supports_search")] bool Supports_Search, + [property: JsonPropertyName("supports_timescale_marks")] bool Supports_Timescale_Marks, + [property: JsonPropertyName("supports_time")] bool Supports_Time); + +internal sealed record UdfSymbolInfo( + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("ticker")] string Ticker, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("type")] string Type, + [property: JsonPropertyName("session")] string Session, + [property: JsonPropertyName("timezone")] string Timezone, + [property: JsonPropertyName("exchange")] string Exchange, + [property: JsonPropertyName("listed_exchange")] string Listed_Exchange, + [property: JsonPropertyName("minmov")] int Minmov, + [property: JsonPropertyName("pricescale")] int Pricescale, + [property: JsonPropertyName("has_intraday")] bool Has_Intraday, + [property: JsonPropertyName("supported_resolutions")] string[] Supported_Resolutions, + [property: JsonPropertyName("volume_precision")] int Volume_Precision, + [property: JsonPropertyName("data_status")] string Data_Status); + +internal sealed record UdfHistory( + [property: JsonPropertyName("s")] string S, + [property: JsonPropertyName("t")] long[] T, + [property: JsonPropertyName("o")] double[] O, + [property: JsonPropertyName("h")] double[] H, + [property: JsonPropertyName("l")] double[] L, + [property: JsonPropertyName("c")] double[] C, + [property: JsonPropertyName("v")] double[] V); + +internal sealed record UdfHistoryNoData( + [property: JsonPropertyName("s")] string S, + [property: JsonPropertyName("nextTime")] long? NextTime); + +internal interface IBarStore +{ + IReadOnlyList GetBars(string symbol, string resolution, long from, long to); + + long? NextBarTime(string symbol, string resolution, long to); +} +``` + +The UDF contract has no slot for custom indicator series β€” the Charting Library expects indicators +to be computed client-side by its own study engine. To plot a **TaLibStandard-computed** series you +either register a custom study that fetches from your own endpoint, or (far simpler) use +Lightweight Charts. + +### 3.3 Serving an indicator series + +This is where `BegIdx` earns its keep. One endpoint, one loop, no off-by-one: + +```csharp +app.MapGet("/indicators/rsi", (string symbol, string resolution, int period, IBarStore store) => +{ + IReadOnlyList bars = store.GetBars(symbol, resolution, long.MinValue, long.MaxValue); + double[] close = bars.Select(b => b.Close).ToArray(); + + RsiResult rsi = TAMath.Rsi(0, close.Length - 1, close, period); + if (rsi.RetCode != RetCode.Success) + { + return Results.Problem($"RSI failed: {rsi.RetCode}", statusCode: StatusCodes.Status422UnprocessableEntity); + } + + // Output element k belongs to input bar rsi.BegIdx + k. + LinePoint[] points = new LinePoint[rsi.NBElement]; + for (int k = 0; k < rsi.NBElement; k++) + { + Bar bar = bars[rsi.BegIdx + k]; + points[k] = new LinePoint(bar.Time, rsi.Real[k]); + } + + return Results.Json(points); +}); + +internal sealed record LinePoint( + [property: JsonPropertyName("time")] long Time, + [property: JsonPropertyName("value")] double Value); +``` + +Note what this does **not** do: it does not emit points for the warm-up bars. Lightweight Charts +draws a line series from a sparse, ascending-by-time array, so simply omitting them is correct and +cheaper than sending `null`s. The warm-up region is `[0, BegIdx)` β€” never send `0.0` there. + +### 3.4 Lightweight Charts + +Apache-2.0, no licence agreement, and small enough to ship in a dashboard. It renders candles and +line series from plain JSON, which is exactly what Β§3.3 produces. + +The snippet below uses the v5 series API (`chart.addSeries(CandlestickSeries)`) and assumes a +bundler resolves the bare `lightweight-charts` specifier. On v4 the calls are +`chart.addCandlestickSeries()` and `chart.addLineSeries()`; without a bundler, load the standalone +build and use the `LightweightCharts` global instead. + +```html +
+ +``` + +`time` is UNIX seconds for intraday series and `"YYYY-MM-DD"` for daily series β€” the same convention +as the UDF `t` array, which is why the two endpoints compose without a translation layer. + +For streaming updates, push new bars over SignalR and call `series.update(bar)` β€” see the +[real-time sample](../../samples/TechnicalAnalysis.Samples.RealTime). + +--- + +## 4. Consuming TradingView webhook alerts + +TradingView alerts can POST a body to a URL. That is the standard way to let a TradingView-authored +signal trigger a .NET strategy. + +### 4.1 The alert payload + +The alert message is a free-text field; make it JSON and template it with Pine placeholders: + +```json +{ + "id": "{{timenow}}-{{ticker}}-{{interval}}", + "secret": "REPLACE_WITH_YOUR_SHARED_SECRET", + "ticker": "{{ticker}}", + "interval": "{{interval}}", + "action": "{{strategy.order.action}}", + "price": {{close}}, + "time": "{{timenow}}" +} +``` + +TradingView substitutes the `{{…}}` placeholders and POSTs the result with +`Content-Type: text/plain` (not `application/json`), so read the raw body rather than relying on +model binding. + +### 4.2 The endpoint + +```csharp +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; + +app.MapPost("/webhooks/tradingview", async ( + HttpRequest request, + IReplayGuard replayGuard, + IConfiguration configuration, + CancellationToken cancellationToken) => +{ + string expectedSecret = configuration["TradingView:WebhookSecret"] + ?? throw new InvalidOperationException("TradingView:WebhookSecret is not configured."); + + using StreamReader reader = new(request.Body, Encoding.UTF8); + string body = await reader.ReadToEndAsync(cancellationToken); + + TradingViewAlert? alert = JsonSerializer.Deserialize(body); + if (alert is null) + { + return Results.BadRequest(); + } + + // 1. Shared secret, compared in constant time. + if (!CryptographicOperations.FixedTimeEquals( + Encoding.UTF8.GetBytes(alert.Secret), + Encoding.UTF8.GetBytes(expectedSecret))) + { + return Results.Unauthorized(); + } + + // 2. Freshness: reject anything older than the alert's useful life. + if (DateTimeOffset.UtcNow - alert.Time > TimeSpan.FromMinutes(2)) + { + return Results.StatusCode(StatusCodes.Status408RequestTimeout); + } + + // 3. Replay protection: each alert id may be consumed exactly once. + if (!replayGuard.TryConsume(alert.Id)) + { + return Results.Conflict(); + } + + // 4. Hand off to the strategy. Return fast; do the work in the background. + return Results.Accepted(); +}); + +internal sealed record TradingViewAlert( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("secret")] string Secret, + [property: JsonPropertyName("ticker")] string Ticker, + [property: JsonPropertyName("interval")] string Interval, + [property: JsonPropertyName("action")] string Action, + [property: JsonPropertyName("price")] decimal Price, + [property: JsonPropertyName("time")] DateTimeOffset Time); + +internal interface IReplayGuard +{ + bool TryConsume(string alertId); +} +``` + +### 4.3 Security notes + +TradingView webhooks are **unauthenticated HTTP POSTs to a public URL**. Treat the endpoint as +hostile-facing. + +- **HTTPS only.** TradingView will not call plain HTTP endpoints, and the shared secret travels in + the body. +- **Shared secret in the body, compared in constant time.** There is no signature header and no HMAC + from TradingView, so a secret in the payload is what you have. Compare with + `CryptographicOperations.FixedTimeEquals`, never with `==` or `string.Equals`. +- **Never log the raw body.** It contains the secret. +- **Unguessable path.** `/webhooks/tradingview/8f3c…` costs nothing and removes you from + opportunistic scanning. +- **IP allowlist.** TradingView publishes the egress IPs its alert servers use. Put them in + configuration β€” they change, and a hard-coded list becomes an outage: + + ```csharp + string[] allowed = configuration.GetSection("TradingView:AllowedIPs").Get() ?? []; + IPAddress? remote = request.HttpContext.Connection.RemoteIpAddress; + if (allowed.Length > 0 && (remote is null || !allowed.Contains(remote.ToString()))) + { + return Results.Forbid(); + } + ``` + + Behind a reverse proxy or CDN you must configure `ForwardedHeadersOptions` (and its + `KnownProxies`/`KnownNetworks`) first, or `RemoteIpAddress` will be your proxy. Check TradingView's + current published list before deploying; do not copy an IP list out of a blog post. +- **Replay protection.** Alerts are retried and can be duplicated. Give each alert a unique `id` + (the `{{timenow}}-{{ticker}}-{{interval}}` template above works) and consume it exactly once β€” + a `MemoryCache` with the freshness window as TTL is sufficient for a single instance; use Redis + with `SET key value NX EX 120` for a cluster. +- **Timestamp freshness.** Reject anything older than a couple of minutes so a captured request + cannot be replayed later, even against a fresh replay-guard store. +- **Rate limit.** Use ASP.NET Core's built-in rate limiting on this endpoint; a fixed window of a + few requests per second per IP is generous for alerts and stops floods. +- **Return fast, act asynchronously.** TradingView times out quickly and retries. Enqueue the order + intent and return `202 Accepted`; do not place the order inside the request. +- **Never trust the payload's prices for execution.** `{{close}}` is TradingView's view of the + market, possibly from a different feed than your broker's. Re-validate against your own data + before sending an order β€” and recompute the confirming indicator with TaLibStandard on your own + bars. + +--- + +## 5. Samples + +Two runnable samples in this repository put the pieces together. Both use deterministic, seeded +synthetic data and run fully offline: + +- [`samples/TechnicalAnalysis.Samples.RealTime`](../../samples/TechnicalAnalysis.Samples.RealTime) β€” + streaming bar ingestion, incremental recomputation, and pushing series to a browser chart. +- [`samples/TechnicalAnalysis.Samples.Backtesting`](../../samples/TechnicalAnalysis.Samples.Backtesting) β€” + bar-by-bar strategy evaluation with correct `BegIdx` alignment and no look-ahead. + +--- + +## See also + +- [Getting started](getting-started.md) β€” installation, `RetCode`/`BegIdx`/`NBElement`, pitfalls. +- [Complete indicator reference](../indicators/README.md) β€” all 159 entry points. +- [`Atypical.TechnicalAnalysis.Functions` API reference](../functions/Atypical.TechnicalAnalysis.Functions.md) +- [`Atypical.TechnicalAnalysis.Candles` API reference](../candles/Atypical.TechnicalAnalysis.Candles.md) diff --git a/docs/indicators/README.md b/docs/indicators/README.md new file mode 100644 index 00000000..2335af4d --- /dev/null +++ b/docs/indicators/README.md @@ -0,0 +1,296 @@ +# TaLibStandard Indicator Reference + +Complete, machine-generated catalog of every public entry point in TaLibStandard. **Do not edit this file by hand** - regenerate it with: + +```bash +python3 tools/generate-indicator-catalog.py +``` + +The generator parses `src/TechnicalAnalysis.Functions/*/TAMath.cs` and `src/TechnicalAnalysis.Candles/*/TACandle.cs` directly, so this page cannot drift from the source. It fails loudly if a newly added indicator is not classified in its mapping table. + +## What is counted here + +| Surface | Entry points | Public `static` overloads | +| --- | ---: | ---: | +| `TAMath` (technical indicators, `TechnicalAnalysis.Functions`) | 98 | 208 | +| `TACandle` (candlestick patterns, `TechnicalAnalysis.Candles`) | 61 | 68 | +| **Total** | **159** | **276** | + +An *entry point* is a distinct public method name. There are **98 indicator entry points** and **61 candlestick pattern entry points**, i.e. **159 distinct methods**. Counting every callable `public static` overload (the `double[]` and `float[]` pairs, plus the default-argument convenience overloads) gives **276** methods, of which **104** are `float[]` overloads. When you see "200+ indicators" advertised for TA-Lib ports, that figure is the overload count, not the entry-point count - the honest numbers for TaLibStandard are 98 / 61 / 276. + +### Per category + +| Category | Entry points | +| --- | ---: | +| [Overlap Studies](#overlap-studies) | 18 | +| [Momentum Indicators](#momentum-indicators) | 30 | +| [Volume Indicators](#volume-indicators) | 3 | +| [Volatility Indicators](#volatility-indicators) | 3 | +| [Price Transform](#price-transform) | 4 | +| [Cycle Indicators](#cycle-indicators) | 5 | +| [Pattern Recognition](#pattern-recognition) | 61 | +| [Statistic Functions](#statistic-functions) | 9 | +| [Math Transform](#math-transform) | 15 | +| [Math Operators](#math-operators) | 11 | +| **Total** | **159** | + +### Overloads, precision and generic math + +- **`double[]` and `float[]` overloads.** Almost every `TAMath` method is declared twice: once taking `double[]` inputs and once taking `float[]`. The `float[]` overload widens its inputs to `double[]` and calls the `double[]` implementation, so it costs one extra allocation and copy per input array and returns exactly the same `*Result` type. All computation is performed in `double`, and every output array is `double[]` (or `int[]`) regardless of the input type. +- **Convenience overloads.** A handful of entry points expose a shorter overload that hard-codes the TA-Lib default instead of declaring an optional parameter. Both shapes are listed in the *Signature* column below. +- **Generic-math candlesticks.** `TACandle` methods are generic: `TACandle.CdlDoji(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` where `T : IFloatingPoint`. That constraint is satisfied by `double`, `float`, `decimal`, `Half` and `System.Runtime.InteropServices.NFloat`, so candlestick detection runs directly on `decimal[]` price arrays with no conversion. All candlestick entry points return `CandleIndicatorResult`, whose `int[] Integers` array holds `0` (no pattern), `+100` (bullish) or `-100` (bearish). + +### Reading the result objects + +Every result record derives from `TechnicalAnalysis.Common.IndicatorResult` and carries three metadata members: `RetCode RetCode`, `int BegIdx` and `int NBElement` (note the capital `B`). **Output element `k` corresponds to input bar `BegIdx + k`, for `k` in `[0, NBElement)`.** Elements at index `>= NBElement` are meaningless zeros. See [the getting-started guide](../guides/getting-started.md) for a fully worked alignment example. + +The *Outputs* column names the properties that expose the result arrays. `Real` comes from `SingleOutputResult`; multi-output records expose named properties (`RealUpperBand`, `SlowK`, `MacdSignal`, ...) that project the protected `Real0`/`Real1`/`Real2` slots in output order. + +## Known defects + +Three entry points below currently return wrong numbers. They fail quietly - `RetCode.Success`, a plausible `NBElement`, and a value you would chart - so they are called out here and marked in the tables that follow. Everything not listed here was checked against the same inputs and behaves. + +| Defect | Affected entry points | Detail | +| --- | --- | --- | +| **`Atr` never divides its running average** | `Atr` | The main output loop divides the emitted value by `period` but never the accumulator, so the running average is multiplied by `period - 1` on every bar. Measured on a series whose true range is exactly `2.0` every bar, `Atr(..., 14)` returns `2, 2, 26.142857, 340, 4420.142857, 57462, 747006.142857, 9711080, ...`; on a 1500-bar series it reaches `+inf` by bar 300. Only the first two outputs are usable. `Natr` is **not** affected. Workaround: Wilder-smooth `TrueRange` yourself. Source: `src/TechnicalAnalysis.Functions/Atr/TAFunc.cs` | +| **`TA_INT_EMA` seeds itself low** | `Ema`, `Macd`, `MacdExt`, `MacdFix`, `Dema`, `Tema`, `T3`, `Apo`, `Ppo`, `Trix` | The seed loop sums `period - 1` inputs and divides by `period` (upstream TA-Lib sums `period` of them), and the loop that follows applies one extra smoothing step. `Ema` over a constant series of `100` with `timePeriod: 20` returns `95.476190` instead of `100`. The error decays with the smoothing factor, so it distorts the bars just after warm-up rather than the steady state; raising `TACore.Globals.UnstablePeriod[FuncUnstId.Ema]` discards them. Source: `src/TechnicalAnalysis.Functions/TAFunc.cs`, `TA_INT_EMA` | +| **`Rsi` returns `NaN` on a perfectly flat series** | `Rsi` | There is no zero guard on `prevGain + prevLoss`, so a window with no price change divides by zero. The call still reports `RetCode.Success` with a non-zero `NBElement`, and every element is `NaN`. Trigger: a halted instrument, or a synthetic constant series. Guard consumers with `double.IsFinite`. Source: `src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs` | + +See [Getting started](../guides/getting-started.md#10-known-library-defects) for the two habits that make these survivable, [Backtesting](../guides/backtesting.md#-limitations--read-before-believing-any-number) and [Real-time streaming](../guides/real-time-streaming.md#-known-library-defects-visible-in-this-sample) for what they do to a running system, and [TradingView integration](../guides/tradingview-integration.md#0-known-library-defects) for what they mean when you are reconciling against a chart. + +## Overlap Studies + +Trend-following overlays plotted on the same scale as price. **18 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `BollingerBands` | `BollingerBandsResult TAMath.BollingerBands(int startIdx, int endIdx, double[] real, int timePeriod = 5, double nbDevUp = 2.0, double nbDevDn = 2.0, MAType maType = MAType.Sma)` | `int timePeriod = 5`
`double nbDevUp = 2.0`
`double nbDevDn = 2.0`
`MAType maType = MAType.Sma` | `RealUpperBand`, `RealMiddleBand`, `RealLowerBand` | Calculates Bollinger Bands for a price series. | [TAMath.BollingerBands](../functions/TAMath.BollingerBands.md) | +| `Dema` | `DemaResult TAMath.Dema(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Double Exponential Moving Average (DEMA) for the specified range of data. | [TAMath.Dema](../functions/TAMath.Dema.md) | +| `Ema` | `EmaResult TAMath.Ema(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Exponential Moving Average (EMA) for the specified range of data. | [TAMath.Ema](../functions/TAMath.Ema.md) | +| `HtTrendline` | `HtTrendlineResult TAMath.HtTrendline(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the Hilbert Transform - Instantaneous Trendline for the input price data. | [TAMath.HtTrendline](../functions/TAMath.HtTrendline.md) | +| `Kama` | `KamaResult TAMath.Kama(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the Kaufman Adaptive Moving Average (KAMA) for the specified range of data. | [TAMath.Kama](../functions/TAMath.Kama.md) | +| `Mama` | `MamaResult TAMath.Mama(int startIdx, int endIdx, double[] real, double fastLimit, double slowLimit)`
`MamaResult TAMath.Mama(int startIdx, int endIdx, double[] real)` | `double fastLimit`
`double slowLimit` | `MAMA`, `FAMA` | Calculates the MESA Adaptive Moving Average (MAMA) for the input price data. | [TAMath.Mama](../functions/TAMath.Mama.md) | +| `MidPoint` | `MidPointResult TAMath.MidPoint(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the midpoint value over a specified time period (MIDPOINT). | [TAMath.MidPoint](../functions/TAMath.MidPoint.md) | +| `MidPrice` | `MidPriceResult TAMath.MidPrice(int startIdx, int endIdx, double[] high, double[] low, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the midpoint price over a specified time period (MIDPRICE). | [TAMath.MidPrice](../functions/TAMath.MidPrice.md) | +| `MovingAverage` | `MovingAverageResult TAMath.MovingAverage(int startIdx, int endIdx, double[] real, int timePeriod = 30, MAType maType = MAType.Sma)` | `int timePeriod = 30`
`MAType maType = MAType.Sma` | `Real` | Calculates a Moving Average of the specified type for the input price data. | [TAMath.MovingAverage](../functions/TAMath.MovingAverage.md) | +| `MovingAverageVariablePeriod` | `MovingAverageVariablePeriodResult TAMath.MovingAverageVariablePeriod(int startIdx, int endIdx, double[] real, double[] periods, int minPeriod, int maxPeriod, MAType maType)`
`MovingAverageVariablePeriodResult TAMath.MovingAverageVariablePeriod(int startIdx, int endIdx, double[] real, double[] periods)` | `int minPeriod`
`int maxPeriod`
`MAType maType` | `Real` | Calculates a moving average with variable period lengths (MAVP). | [TAMath.MovingAverageVariablePeriod](../functions/TAMath.MovingAverageVariablePeriod.md) | +| `Sar` | `SarResult TAMath.Sar(int startIdx, int endIdx, double[] high, double[] low, double acceleration = 0.02, double maximum = 0.2)` | `double acceleration = 0.02`
`double maximum = 0.2` | `Real` | Calculates the Parabolic SAR (Stop And Reverse) indicator. | [TAMath.Sar](../functions/TAMath.Sar.md) | +| `SarExt` | `SarExtResult TAMath.SarExt(int startIdx, int endIdx, double[] high, double[] low, double startValue = 0.0, double offsetOnReverse = 0.0, double accelerationInitLong = 0.02, double accelerationLong = 0.02, double accelerationMaxLong = 0.2, double accelerationInitShort = 0.02, double accelerationShort = 0.02, double accelerationMaxShort = 0.2)` | `double startValue = 0.0`
`double offsetOnReverse = 0.0`
`double accelerationInitLong = 0.02`
`double accelerationLong = 0.02`
`double accelerationMaxLong = 0.2`
`double accelerationInitShort = 0.02`
`double accelerationShort = 0.02`
`double accelerationMaxShort = 0.2` | `Real` | Calculates the Parabolic SAR Extended (SAREXT) indicator. | [TAMath.SarExt](../functions/TAMath.SarExt.md) | +| `Sma` | `SmaResult TAMath.Sma(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the Simple Moving Average (SMA) for the input price data. | [TAMath.Sma](../functions/TAMath.Sma.md) | +| `T3` | `T3Result TAMath.T3(int startIdx, int endIdx, double[] real, int timePeriod = 5, double vFactor = 0.7)` | `int timePeriod = 5`
`double vFactor = 0.7` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the T3 Triple Exponential Moving Average for the specified range of data. | [TAMath.T3](../functions/TAMath.T3.md) | +| `Tema` | `TemaResult TAMath.Tema(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Triple Exponential Moving Average (TEMA) for the specified range of data. | [TAMath.Tema](../functions/TAMath.Tema.md) | +| `Trima` | `TrimaResult TAMath.Trima(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the Triangular Moving Average (TRIMA) for the specified range of data. | [TAMath.Trima](../functions/TAMath.Trima.md) | +| `Wma` | `WmaResult TAMath.Wma(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the Weighted Moving Average (WMA) for the specified range of data. | [TAMath.Wma](../functions/TAMath.Wma.md) | +| `ZigZag` | `ZigZagResult TAMath.ZigZag(int startIdx, int endIdx, double[] high, double[] low, double deviation = 5.0)` | `double deviation = 5.0` | `Real` | Calculates the ZigZag indicator which filters out minor price movements to identify significant trends. | [TAMath.ZigZag](../functions/TAMath.ZigZag.md) | + +## Momentum Indicators + +Oscillators and directional-movement measures derived from rate of change. **30 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Adx` | `AdxResult TAMath.Adx(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Average Directional Index (ADX) indicator. | [TAMath.Adx](../functions/TAMath.Adx.md) | +| `Adxr` | `AdxrResult TAMath.Adxr(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Average Directional Movement Index Rating (ADXR) indicator. | [TAMath.Adxr](../functions/TAMath.Adxr.md) | +| `Apo` | `ApoResult TAMath.Apo(int startIdx, int endIdx, double[] real, int fastPeriod = 12, int slowPeriod = 26, MAType maType = MAType.Sma)` | `int fastPeriod = 12`
`int slowPeriod = 26`
`MAType maType = MAType.Sma` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Absolute Price Oscillator (APO) which shows the difference between two moving averages. | [TAMath.Apo](../functions/TAMath.Apo.md) | +| `Aroon` | `AroonResult TAMath.Aroon(int startIdx, int endIdx, double[] high, double[] low, int timePeriod = 14)` | `int timePeriod = 14` | `AroonDown`, `AroonUp` | Calculates the Aroon indicator (Aroon Up and Aroon Down). | [TAMath.Aroon](../functions/TAMath.Aroon.md) | +| `AroonOsc` | `AroonOscResult TAMath.AroonOsc(int startIdx, int endIdx, double[] high, double[] low, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Aroon Oscillator (AROONOSC) which measures the strength of a trend and likelihood of continuation. | [TAMath.AroonOsc](../functions/TAMath.AroonOsc.md) | +| `Bop` | `BopResult TAMath.Bop(int startIdx, int endIdx, double[] open, double[] high, double[] low, double[] close)` | _none_ | `Real` | Calculates the Balance of Power (BOP) indicator. | [TAMath.Bop](../functions/TAMath.Bop.md) | +| `Cci` | `CciResult TAMath.Cci(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Commodity Channel Index (CCI) indicator. | [TAMath.Cci](../functions/TAMath.Cci.md) | +| `Cmo` | `CmoResult TAMath.Cmo(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Chande Momentum Oscillator (CMO) which measures momentum on both up and down days. | [TAMath.Cmo](../functions/TAMath.Cmo.md) | +| `Dx` | `DxResult TAMath.Dx(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod)`
`DxResult TAMath.Dx(int startIdx, int endIdx, double[] high, double[] low, double[] close)` | `int timePeriod` | `Real` | Calculates the Directional Movement Index (DX) indicator. | [TAMath.Dx](../functions/TAMath.Dx.md) | +| `Macd` | `MacdResult TAMath.Macd(int startIdx, int endIdx, double[] real, int optInFastPeriod = 12, int optInSlowPeriod = 26, int optInSignalPeriod = 9)` | `int optInFastPeriod = 12`
`int optInSlowPeriod = 26`
`int optInSignalPeriod = 9` | `MacdValue`, `MacdSignal`, `MacdHist` | [**KNOWN DEFECT**](#known-defects) - Calculates the Moving Average Convergence Divergence (MACD) indicator. | [TAMath.Macd](../functions/TAMath.Macd.md) | +| `MacdExt` | `MacdExtResult TAMath.MacdExt(int startIdx, int endIdx, double[] real, int fastPeriod = 12, MAType fastMAType = MAType.Sma, int slowPeriod = 26, MAType slowMAType = MAType.Sma, int signalPeriod = 9, MAType signalMAType = MAType.Sma)` | `int fastPeriod = 12`
`MAType fastMAType = MAType.Sma`
`int slowPeriod = 26`
`MAType slowMAType = MAType.Sma`
`int signalPeriod = 9`
`MAType signalMAType = MAType.Sma` | `MACD`, `MACDSignal`, `MACDHist` | [**KNOWN DEFECT**](#known-defects) - Calculates the Extended MACD (MACD with controllable MA type) indicator. | [TAMath.MacdExt](../functions/TAMath.MacdExt.md) | +| `MacdFix` | `MacdFixResult TAMath.MacdFix(int startIdx, int endIdx, double[] real, int signalPeriod = 9)` | `int signalPeriod = 9` | `MACD`, `MACDSignal`, `MACDHist` | [**KNOWN DEFECT**](#known-defects) - Calculates the Fixed MACD indicator using the standard 12/26 period configuration. | [TAMath.MacdFix](../functions/TAMath.MacdFix.md) | +| `Mfi` | `MfiResult TAMath.Mfi(int startIdx, int endIdx, double[] high, double[] low, double[] close, double[] volume, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Money Flow Index (MFI) indicator. | [TAMath.Mfi](../functions/TAMath.Mfi.md) | +| `MinusDI` | `MinusDIResult TAMath.MinusDI(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod)`
`MinusDIResult TAMath.MinusDI(int startIdx, int endIdx, double[] high, double[] low, double[] close)` | `int timePeriod` | `Real` | Calculates the Minus Directional Indicator (-DI) which measures the strength of negative price movement. | [TAMath.MinusDI](../functions/TAMath.MinusDI.md) | +| `MinusDM` | `MinusDMResult TAMath.MinusDM(int startIdx, int endIdx, double[] high, double[] low, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Minus Directional Movement (-DM) which measures negative price movement between periods. | [TAMath.MinusDM](../functions/TAMath.MinusDM.md) | +| `Mom` | `MomResult TAMath.Mom(int startIdx, int endIdx, double[] real, int timePeriod = 10)` | `int timePeriod = 10` | `Real` | Calculates the Momentum indicator for the specified range of data. | [TAMath.Mom](../functions/TAMath.Mom.md) | +| `PlusDI` | `PlusDIResult TAMath.PlusDI(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod)`
`PlusDIResult TAMath.PlusDI(int startIdx, int endIdx, double[] high, double[] low, double[] close)` | `int timePeriod` | `Real` | Calculates the Plus Directional Indicator (+DI) which measures the strength of positive price movement. | [TAMath.PlusDI](../functions/TAMath.PlusDI.md) | +| `PlusDM` | `PlusDMResult TAMath.PlusDM(int startIdx, int endIdx, double[] high, double[] low, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Plus Directional Movement (+DM) which measures positive price movement between periods. | [TAMath.PlusDM](../functions/TAMath.PlusDM.md) | +| `Ppo` | `PpoResult TAMath.Ppo(int startIdx, int endIdx, double[] real, int fastPeriod = 12, int slowPeriod = 26, MAType maType = MAType.Sma)` | `int fastPeriod = 12`
`int slowPeriod = 26`
`MAType maType = MAType.Sma` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Percentage Price Oscillator (PPO) which shows the percentage difference between two moving averages. | [TAMath.Ppo](../functions/TAMath.Ppo.md) | +| `Roc` | `RocResult TAMath.Roc(int startIdx, int endIdx, double[] real, int timePeriod = 10)` | `int timePeriod = 10` | `Real` | Calculates the Rate of Change (ROC) indicator for the specified range of data. | [TAMath.Roc](../functions/TAMath.Roc.md) | +| `RocP` | `RocPResult TAMath.RocP(int startIdx, int endIdx, double[] real, int timePeriod = 10)` | `int timePeriod = 10` | `Real` | Calculates the Rate of Change Percentage (ROCP) indicator for the specified range of data. | [TAMath.RocP](../functions/TAMath.RocP.md) | +| `RocR` | `RocRResult TAMath.RocR(int startIdx, int endIdx, double[] real, int timePeriod = 10)` | `int timePeriod = 10` | `Real` | Calculates the Rate of Change Ratio (ROCR) which measures price momentum as a ratio. | [TAMath.RocR](../functions/TAMath.RocR.md) | +| `RocR100` | `RocR100Result TAMath.RocR100(int startIdx, int endIdx, double[] real, int timePeriod = 10)` | `int timePeriod = 10` | `Real` | Calculates the Rate of Change Ratio 100 scale (ROCR100) which measures price momentum as a percentage. | [TAMath.RocR100](../functions/TAMath.RocR100.md) | +| `Rsi` | `RsiResult TAMath.Rsi(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Relative Strength Index (RSI) for the input price data. | [TAMath.Rsi](../functions/TAMath.Rsi.md) | +| `Stoch` | `StochResult TAMath.Stoch(int startIdx, int endIdx, double[] high, double[] low, double[] close, int fastKPeriod = 5, int slowKPeriod = 3, MAType slowKMAType = MAType.Sma, int slowDPeriod = 3, MAType slowDMAType = MAType.Sma)` | `int fastKPeriod = 5`
`int slowKPeriod = 3`
`MAType slowKMAType = MAType.Sma`
`int slowDPeriod = 3`
`MAType slowDMAType = MAType.Sma` | `SlowK`, `SlowD` | Calculates the Stochastic Oscillator (STOCH) which measures momentum by comparing closing price to price range. | [TAMath.Stoch](../functions/TAMath.Stoch.md) | +| `StochF` | `StochFResult TAMath.StochF(int startIdx, int endIdx, double[] high, double[] low, double[] close, int fastKPeriod = 5, int fastDPeriod = 3, MAType fastDMAType = MAType.Sma)` | `int fastKPeriod = 5`
`int fastDPeriod = 3`
`MAType fastDMAType = MAType.Sma` | `FastK`, `FastD` | Calculates the Fast Stochastic (STOCHF) which provides a more responsive version of the stochastic oscillator. | [TAMath.StochF](../functions/TAMath.StochF.md) | +| `StochRsi` | `StochRsiResult TAMath.StochRsi(int startIdx, int endIdx, double[] real, int timePeriod = 14, int fastKPeriod = 5, int fastDPeriod = 3, MAType fastDMAType = MAType.Sma)` | `int timePeriod = 14`
`int fastKPeriod = 5`
`int fastDPeriod = 3`
`MAType fastDMAType = MAType.Sma` | `FastK`, `FastD` | Calculates the Stochastic RSI (STOCHRSI) which applies stochastic calculations to RSI values. | [TAMath.StochRsi](../functions/TAMath.StochRsi.md) | +| `Trix` | `TrixResult TAMath.Trix(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the 1-day Rate-Of-Change (TRIX) indicator. | [TAMath.Trix](../functions/TAMath.Trix.md) | +| `UltOsc` | `UltOscResult TAMath.UltOsc(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod1 = 7, int timePeriod2 = 14, int timePeriod3 = 28)` | `int timePeriod1 = 7`
`int timePeriod2 = 14`
`int timePeriod3 = 28` | `Real` | Calculates the Ultimate Oscillator (ULTOSC) indicator. | [TAMath.UltOsc](../functions/TAMath.UltOsc.md) | +| `WillR` | `WillRResult TAMath.WillR(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Williams' %R (WILLR) indicator. | [TAMath.WillR](../functions/TAMath.WillR.md) | + +## Volume Indicators + +Indicators that combine price with traded volume. **3 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Ad` | `AdResult TAMath.Ad(int startIdx, int endIdx, double[] high, double[] low, double[] close, double[] volume)` | _none_ | `Real` | Calculates the Accumulation/Distribution (A/D) indicator. | [TAMath.Ad](../functions/TAMath.Ad.md) | +| `AdOsc` | `AdOscResult TAMath.AdOsc(int startIdx, int endIdx, double[] high, double[] low, double[] close, double[] volume, int fastPeriod = 3, int slowPeriod = 10)` | `int fastPeriod = 3`
`int slowPeriod = 10` | `Real` | Calculates the Chaikin Accumulation/Distribution Oscillator (ADOSC). | [TAMath.AdOsc](../functions/TAMath.AdOsc.md) | +| `Obv` | `ObvResult TAMath.Obv(int startIdx, int endIdx, double[] real, double[] volume)` | _none_ | `Real` | Calculates the On Balance Volume (OBV) indicator. | [TAMath.Obv](../functions/TAMath.Obv.md) | + +## Volatility Indicators + +True-range based measures of dispersion. **3 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Atr` | `AtrResult TAMath.Atr(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | [**KNOWN DEFECT**](#known-defects) - Calculates the Average True Range (ATR) indicator. | [TAMath.Atr](../functions/TAMath.Atr.md) | +| `Natr` | `NatrResult TAMath.Natr(int startIdx, int endIdx, double[] high, double[] low, double[] close, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Normalized Average True Range (NATR) indicator. | [TAMath.Natr](../functions/TAMath.Natr.md) | +| `TrueRange` | `TrueRangeResult TAMath.TrueRange(int startIdx, int endIdx, double[] high, double[] low, double[] close)` | _none_ | `Real` | Calculates the True Range (TRANGE) indicator. | [TAMath.TrueRange](../functions/TAMath.TrueRange.md) | + +## Price Transform + +Single-bar arithmetic combinations of open/high/low/close. **4 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `AvgPrice` | `AvgPriceResult TAMath.AvgPrice(int startIdx, int endIdx, double[] open, double[] high, double[] low, double[] close)` | _none_ | `Real` | Calculates the average price of a security for each period. | [TAMath.AvgPrice](../functions/TAMath.AvgPrice.md) | +| `MedPrice` | `MedPriceResult TAMath.MedPrice(int startIdx, int endIdx, double[] high, double[] low)` | _none_ | `Real` | Calculates the median price of a security for each period. | [TAMath.MedPrice](../functions/TAMath.MedPrice.md) | +| `TypPrice` | `TypPriceResult TAMath.TypPrice(int startIdx, int endIdx, double[] high, double[] low, double[] close)` | _none_ | `Real` | Calculates the typical price of a security for each period. | [TAMath.TypPrice](../functions/TAMath.TypPrice.md) | +| `WclPrice` | `WclPriceResult TAMath.WclPrice(int startIdx, int endIdx, double[] high, double[] low, double[] close)` | _none_ | `Real` | Calculates the weighted close price of a security for each period. | [TAMath.WclPrice](../functions/TAMath.WclPrice.md) | + +## Cycle Indicators + +Hilbert Transform cycle analysis (Ehlers). **5 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `HtDcPeriod` | `HtDcPeriodResult TAMath.HtDcPeriod(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the Hilbert Transform - Dominant Cycle Period for the input price data. | [TAMath.HtDcPeriod](../functions/TAMath.HtDcPeriod.md) | +| `HtDcPhase` | `HtDcPhaseResult TAMath.HtDcPhase(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the Hilbert Transform - Dominant Cycle Phase for the input price data. | [TAMath.HtDcPhase](../functions/TAMath.HtDcPhase.md) | +| `HtPhasor` | `HtPhasorResult TAMath.HtPhasor(int startIdx, int endIdx, double[] real)` | _none_ | `InPhase`, `Quadrature` | Calculates the Hilbert Transform - Phasor Components for the input price data. | [TAMath.HtPhasor](../functions/TAMath.HtPhasor.md) | +| `HtSine` | `HtSineResult TAMath.HtSine(int startIdx, int endIdx, double[] real)` | _none_ | `Sine`, `LeadSine` | Calculates the Hilbert Transform - SineWave indicator for the input price data. | [TAMath.HtSine](../functions/TAMath.HtSine.md) | +| `HtTrendMode` | `HtTrendModeResult TAMath.HtTrendMode(int startIdx, int endIdx, double[] real)` | _none_ | `Integers` | Calculates the Hilbert Transform - Trend vs Cycle Mode for the input price data. | [TAMath.HtTrendMode](../functions/TAMath.HtTrendMode.md) | + +## Pattern Recognition + +Candlestick pattern detection. Every entry point is generic over `T : IFloatingPoint`. **61 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Cdl2Crows` | `CandleIndicatorResult TACandle.Cdl2Crows(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Two Crows (Pattern Recognition) | [TACandle.Cdl2Crows](../candles/TACandle.Cdl2Crows_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `Cdl3BlackCrows` | `CandleIndicatorResult TACandle.Cdl3BlackCrows(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Three Black Crows (Pattern Recognition) | [TACandle.Cdl3BlackCrows](../candles/TACandle.Cdl3BlackCrows_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `Cdl3Inside` | `CandleIndicatorResult TACandle.Cdl3Inside(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Three Inside Up/Down (Pattern Recognition) | [TACandle.Cdl3Inside](../candles/TACandle.Cdl3Inside_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `Cdl3LineStrike` | `CandleIndicatorResult TACandle.Cdl3LineStrike(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Three-Line Strike (Pattern Recognition) | [TACandle.Cdl3LineStrike](../candles/TACandle.Cdl3LineStrike_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `Cdl3Outside` | `CandleIndicatorResult TACandle.Cdl3Outside(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Three Outside Up/Down (Pattern Recognition) | [TACandle.Cdl3Outside](../candles/TACandle.Cdl3Outside_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `Cdl3StarsInSouth` | `CandleIndicatorResult TACandle.Cdl3StarsInSouth(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Three Stars In The South (Pattern Recognition) | [TACandle.Cdl3StarsInSouth](../candles/TACandle.Cdl3StarsInSouth_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `Cdl3WhiteSoldiers` | `CandleIndicatorResult TACandle.Cdl3WhiteSoldiers(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Three Advancing White Soldiers (Pattern Recognition) | [TACandle.Cdl3WhiteSoldiers](../candles/TACandle.Cdl3WhiteSoldiers_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlAbandonedBaby` | `CandleIndicatorResult TACandle.CdlAbandonedBaby(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlAbandonedBaby(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Abandoned Baby (Pattern Recognition) | [TACandle.CdlAbandonedBaby](../candles/TACandle.CdlAbandonedBaby.md) | +| `CdlAdvanceBlock` | `CandleIndicatorResult TACandle.CdlAdvanceBlock(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Advance Block (Pattern Recognition) | [TACandle.CdlAdvanceBlock](../candles/TACandle.CdlAdvanceBlock_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlBeltHold` | `CandleIndicatorResult TACandle.CdlBeltHold(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Belt-hold (Pattern Recognition) | [TACandle.CdlBeltHold](../candles/TACandle.CdlBeltHold_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlBreakaway` | `CandleIndicatorResult TACandle.CdlBreakaway(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Breakaway (Pattern Recognition) | [TACandle.CdlBreakaway](../candles/TACandle.CdlBreakaway_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlClosingMarubozu` | `CandleIndicatorResult TACandle.CdlClosingMarubozu(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Closing Marubozu (Pattern Recognition) | [TACandle.CdlClosingMarubozu](../candles/TACandle.CdlClosingMarubozu_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlConcealBabySwallow` | `CandleIndicatorResult TACandle.CdlConcealBabySwallow(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Concealing Baby Swallow (Pattern Recognition) | [TACandle.CdlConcealBabySwallow](../candles/TACandle.CdlConcealBabySwallow_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlCounterAttack` | `CandleIndicatorResult TACandle.CdlCounterAttack(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Counterattack (Pattern Recognition) | [TACandle.CdlCounterAttack](../candles/TACandle.CdlCounterAttack_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlDarkCloudCover` | `CandleIndicatorResult TACandle.CdlDarkCloudCover(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlDarkCloudCover(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Dark Cloud Cover (Pattern Recognition) | [TACandle.CdlDarkCloudCover](../candles/TACandle.CdlDarkCloudCover.md) | +| `CdlDoji` | `CandleIndicatorResult TACandle.CdlDoji(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Doji (Pattern Recognition) | [TACandle.CdlDoji](../candles/TACandle.CdlDoji_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlDojiStar` | `CandleIndicatorResult TACandle.CdlDojiStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Doji Star (Pattern Recognition) | [TACandle.CdlDojiStar](../candles/TACandle.CdlDojiStar_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlDragonflyDoji` | `CandleIndicatorResult TACandle.CdlDragonflyDoji(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Dragonfly Doji (Pattern Recognition) | [TACandle.CdlDragonflyDoji](../candles/TACandle.CdlDragonflyDoji_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlEngulfing` | `CandleIndicatorResult TACandle.CdlEngulfing(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Engulfing Pattern (Pattern Recognition) | [TACandle.CdlEngulfing](../candles/TACandle.CdlEngulfing_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlEveningDojiStar` | `CandleIndicatorResult TACandle.CdlEveningDojiStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlEveningDojiStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Evening Doji Star (Pattern Recognition) | [TACandle.CdlEveningDojiStar](../candles/TACandle.CdlEveningDojiStar.md) | +| `CdlEveningStar` | `CandleIndicatorResult TACandle.CdlEveningStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlEveningStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Evening Star (Pattern Recognition) | [TACandle.CdlEveningStar](../candles/TACandle.CdlEveningStar.md) | +| `CdlGapSideSideWhite` | `CandleIndicatorResult TACandle.CdlGapSideSideWhite(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Up/Down-gap side-by-side white lines (Pattern Recognition) | [TACandle.CdlGapSideSideWhite](../candles/TACandle.CdlGapSideSideWhite_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlGravestoneDoji` | `CandleIndicatorResult TACandle.CdlGravestoneDoji(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Gravestone Doji (Pattern Recognition) | [TACandle.CdlGravestoneDoji](../candles/TACandle.CdlGravestoneDoji_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHammer` | `CandleIndicatorResult TACandle.CdlHammer(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Hammer (Pattern Recognition) | [TACandle.CdlHammer](../candles/TACandle.CdlHammer_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHangingMan` | `CandleIndicatorResult TACandle.CdlHangingMan(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Hanging Man (Pattern Recognition) | [TACandle.CdlHangingMan](../candles/TACandle.CdlHangingMan_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHarami` | `CandleIndicatorResult TACandle.CdlHarami(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Harami Pattern (Pattern Recognition) | [TACandle.CdlHarami](../candles/TACandle.CdlHarami_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHaramiCross` | `CandleIndicatorResult TACandle.CdlHaramiCross(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Harami Cross Pattern (Pattern Recognition) | [TACandle.CdlHaramiCross](../candles/TACandle.CdlHaramiCross_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHighWave` | `CandleIndicatorResult TACandle.CdlHighWave(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | High-Wave Candle (Pattern Recognition) | [TACandle.CdlHighWave](../candles/TACandle.CdlHighWave_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHikkake` | `CandleIndicatorResult TACandle.CdlHikkake(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Hikkake Pattern (Pattern Recognition) | [TACandle.CdlHikkake](../candles/TACandle.CdlHikkake_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHikkakeMod` | `CandleIndicatorResult TACandle.CdlHikkakeMod(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Modified Hikkake Pattern (Pattern Recognition) | [TACandle.CdlHikkakeMod](../candles/TACandle.CdlHikkakeMod_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlHomingPigeon` | `CandleIndicatorResult TACandle.CdlHomingPigeon(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Homing Pigeon (Pattern Recognition) | [TACandle.CdlHomingPigeon](../candles/TACandle.CdlHomingPigeon_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlIdentical3Crows` | `CandleIndicatorResult TACandle.CdlIdentical3Crows(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Identical Three Crows (Pattern Recognition) | [TACandle.CdlIdentical3Crows](../candles/TACandle.CdlIdentical3Crows_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlInNeck` | `CandleIndicatorResult TACandle.CdlInNeck(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | In-Neck Pattern (Pattern Recognition) | [TACandle.CdlInNeck](../candles/TACandle.CdlInNeck_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlInvertedHammer` | `CandleIndicatorResult TACandle.CdlInvertedHammer(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Inverted Hammer (Pattern Recognition) | [TACandle.CdlInvertedHammer](../candles/TACandle.CdlInvertedHammer_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlKicking` | `CandleIndicatorResult TACandle.CdlKicking(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Kicking (Pattern Recognition) | [TACandle.CdlKicking](../candles/TACandle.CdlKicking_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlKickingByLength` | `CandleIndicatorResult TACandle.CdlKickingByLength(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Kicking - bull/bear determined by the longer marubozu (Pattern Recognition) | [TACandle.CdlKickingByLength](../candles/TACandle.CdlKickingByLength_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlLadderBottom` | `CandleIndicatorResult TACandle.CdlLadderBottom(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Ladder Bottom (Pattern Recognition) | [TACandle.CdlLadderBottom](../candles/TACandle.CdlLadderBottom_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlLongLeggedDoji` | `CandleIndicatorResult TACandle.CdlLongLeggedDoji(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Long Legged Doji (Pattern Recognition) | [TACandle.CdlLongLeggedDoji](../candles/TACandle.CdlLongLeggedDoji_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlLongLine` | `CandleIndicatorResult TACandle.CdlLongLine(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Long Line Candle (Pattern Recognition) | [TACandle.CdlLongLine](../candles/TACandle.CdlLongLine_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlMarubozu` | `CandleIndicatorResult TACandle.CdlMarubozu(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Marubozu (Pattern Recognition) | [TACandle.CdlMarubozu](../candles/TACandle.CdlMarubozu_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlMatHold` | `CandleIndicatorResult TACandle.CdlMatHold(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlMatHold(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Mat Hold (Pattern Recognition) | [TACandle.CdlMatHold](../candles/TACandle.CdlMatHold.md) | +| `CdlMatchingLow` | `CandleIndicatorResult TACandle.CdlMatchingLow(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Matching Low (Pattern Recognition) | [TACandle.CdlMatchingLow](../candles/TACandle.CdlMatchingLow_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlMorningDojiStar` | `CandleIndicatorResult TACandle.CdlMorningDojiStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlMorningDojiStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Morning Doji Star (Pattern Recognition) | [TACandle.CdlMorningDojiStar](../candles/TACandle.CdlMorningDojiStar.md) | +| `CdlMorningStar` | `CandleIndicatorResult TACandle.CdlMorningStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close, T penetration)`
`CandleIndicatorResult TACandle.CdlMorningStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | `T penetration` | `Integers` | Morning Star (Pattern Recognition) | [TACandle.CdlMorningStar](../candles/TACandle.CdlMorningStar.md) | +| `CdlOnNeck` | `CandleIndicatorResult TACandle.CdlOnNeck(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | On-Neck Pattern (Pattern Recognition) | [TACandle.CdlOnNeck](../candles/TACandle.CdlOnNeck_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlPiercing` | `CandleIndicatorResult TACandle.CdlPiercing(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Piercing Pattern (Pattern Recognition) | [TACandle.CdlPiercing](../candles/TACandle.CdlPiercing_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlRickshawMan` | `CandleIndicatorResult TACandle.CdlRickshawMan(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Rickshaw Man (Pattern Recognition) | [TACandle.CdlRickshawMan](../candles/TACandle.CdlRickshawMan_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlRiseFall3Methods` | `CandleIndicatorResult TACandle.CdlRiseFall3Methods(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Rising/Falling Three Methods (Pattern Recognition) | [TACandle.CdlRiseFall3Methods](../candles/TACandle.CdlRiseFall3Methods_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlSeparatingLines` | `CandleIndicatorResult TACandle.CdlSeparatingLines(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Separating Lines (Pattern Recognition) | [TACandle.CdlSeparatingLines](../candles/TACandle.CdlSeparatingLines_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlShootingStar` | `CandleIndicatorResult TACandle.CdlShootingStar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Shooting Star (Pattern Recognition) | [TACandle.CdlShootingStar](../candles/TACandle.CdlShootingStar_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlShortLine` | `CandleIndicatorResult TACandle.CdlShortLine(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Short Line Candle (Pattern Recognition) | [TACandle.CdlShortLine](../candles/TACandle.CdlShortLine_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlSpinningTop` | `CandleIndicatorResult TACandle.CdlSpinningTop(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Spinning Top (Pattern Recognition) | [TACandle.CdlSpinningTop](../candles/TACandle.CdlSpinningTop_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlStalledPattern` | `CandleIndicatorResult TACandle.CdlStalledPattern(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Stalled Pattern (Pattern Recognition) | [TACandle.CdlStalledPattern](../candles/TACandle.CdlStalledPattern_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlStickSandwich` | `CandleIndicatorResult TACandle.CdlStickSandwich(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Stick Sandwich (Pattern Recognition) | [TACandle.CdlStickSandwich](../candles/TACandle.CdlStickSandwich_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlTakuri` | `CandleIndicatorResult TACandle.CdlTakuri(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Takuri (Dragonfly Doji with very long lower shadow) (Pattern Recognition) | [TACandle.CdlTakuri](../candles/TACandle.CdlTakuri_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlTasukiGap` | `CandleIndicatorResult TACandle.CdlTasukiGap(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Tasuki Gap (Pattern Recognition) | [TACandle.CdlTasukiGap](../candles/TACandle.CdlTasukiGap_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlThrusting` | `CandleIndicatorResult TACandle.CdlThrusting(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Thrusting Pattern (Pattern Recognition) | [TACandle.CdlThrusting](../candles/TACandle.CdlThrusting_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlTristar` | `CandleIndicatorResult TACandle.CdlTristar(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Tristar Pattern (Pattern Recognition) | [TACandle.CdlTristar](../candles/TACandle.CdlTristar_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlUnique3River` | `CandleIndicatorResult TACandle.CdlUnique3River(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Unique 3 River (Pattern Recognition) | [TACandle.CdlUnique3River](../candles/TACandle.CdlUnique3River_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlUpsideGap2Crows` | `CandleIndicatorResult TACandle.CdlUpsideGap2Crows(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Upside Gap Two Crows (Pattern Recognition) | [TACandle.CdlUpsideGap2Crows](../candles/TACandle.CdlUpsideGap2Crows_T_%28int,int,T[],T[],T[],T[]%29.md) | +| `CdlXSideGap3Methods` | `CandleIndicatorResult TACandle.CdlXSideGap3Methods(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` | _none_ | `Integers` | Upside/Downside Gap Three Methods (Pattern Recognition) | [TACandle.CdlXSideGap3Methods](../candles/TACandle.CdlXSideGap3Methods_T_%28int,int,T[],T[],T[],T[]%29.md) | + +## Statistic Functions + +Regression and dispersion statistics over a rolling window. **9 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Beta` | `BetaResult TAMath.Beta(int startIdx, int endIdx, double[] real0, double[] real1, int timePeriod = 5)` | `int timePeriod = 5` | `Real` | Calculates the Beta coefficient between two price series. | [TAMath.Beta](../functions/TAMath.Beta.md) | +| `Correl` | `CorrelResult TAMath.Correl(int startIdx, int endIdx, double[] real0, double[] real1, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the Pearson correlation coefficient between two price series. | [TAMath.Correl](../functions/TAMath.Correl.md) | +| `LinearReg` | `LinearRegResult TAMath.LinearReg(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Linear Regression (LINEARREG) indicator. | [TAMath.LinearReg](../functions/TAMath.LinearReg.md) | +| `LinearRegAngle` | `LinearRegAngleResult TAMath.LinearRegAngle(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Linear Regression Angle (LINEARREG_ANGLE) indicator. | [TAMath.LinearRegAngle](../functions/TAMath.LinearRegAngle.md) | +| `LinearRegIntercept` | `LinearRegInterceptResult TAMath.LinearRegIntercept(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Linear Regression Intercept for the input price data. | [TAMath.LinearRegIntercept](../functions/TAMath.LinearRegIntercept.md) | +| `LinearRegSlope` | `LinearRegSlopeResult TAMath.LinearRegSlope(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Linear Regression Slope for the input price data. | [TAMath.LinearRegSlope](../functions/TAMath.LinearRegSlope.md) | +| `StdDev` | `StdDevResult TAMath.StdDev(int startIdx, int endIdx, double[] real, int timePeriod = 5, double nbDev = 1.0)` | `int timePeriod = 5`
`double nbDev = 1.0` | `Real` | Calculates the standard deviation of a price series. | [TAMath.StdDev](../functions/TAMath.StdDev.md) | +| `Tsf` | `TsfResult TAMath.Tsf(int startIdx, int endIdx, double[] real, int timePeriod = 14)` | `int timePeriod = 14` | `Real` | Calculates the Time Series Forecast (TSF) indicator. | [TAMath.Tsf](../functions/TAMath.Tsf.md) | +| `Variance` | `VarianceResult TAMath.Variance(int startIdx, int endIdx, double[] real, int timePeriod = 5, double nbDev = 1.0)` | `int timePeriod = 5`
`double nbDev = 1.0` | `Real` | Calculates the variance of a price series. | [TAMath.Variance](../functions/TAMath.Variance.md) | + +## Math Transform + +Element-wise transcendental and rounding functions. **15 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Acos` | `AcosResult TAMath.Acos(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the arc cosine (inverse cosine) of input values. | [TAMath.Acos](../functions/TAMath.Acos.md) | +| `Asin` | `AsinResult TAMath.Asin(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the arcsine (inverse sine) of each element in the input array. | [TAMath.Asin](../functions/TAMath.Asin.md) | +| `Atan` | `AtanResult TAMath.Atan(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the arctangent (inverse tangent) of each element in the input array. | [TAMath.Atan](../functions/TAMath.Atan.md) | +| `Ceil` | `CeilResult TAMath.Ceil(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the ceiling (smallest integer greater than or equal to) of each element in the input array. | [TAMath.Ceil](../functions/TAMath.Ceil.md) | +| `Cos` | `CosResult TAMath.Cos(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the trigonometric cosine of each element in the input array. | [TAMath.Cos](../functions/TAMath.Cos.md) | +| `Cosh` | `CoshResult TAMath.Cosh(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the hyperbolic cosine of each element in the input array. | [TAMath.Cosh](../functions/TAMath.Cosh.md) | +| `Exp` | `ExpResult TAMath.Exp(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the exponential (e raised to the power) of each element in the input array. | [TAMath.Exp](../functions/TAMath.Exp.md) | +| `Floor` | `FloorResult TAMath.Floor(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the vector floor function for each element in the input array. | [TAMath.Floor](../functions/TAMath.Floor.md) | +| `Ln` | `LnResult TAMath.Ln(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the natural logarithm (base e) for each element in the input array. | [TAMath.Ln](../functions/TAMath.Ln.md) | +| `Log10` | `Log10Result TAMath.Log10(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the base-10 logarithm for each element in the input array. | [TAMath.Log10](../functions/TAMath.Log10.md) | +| `Sin` | `SinResult TAMath.Sin(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the trigonometric sine for each element in the input array. | [TAMath.Sin](../functions/TAMath.Sin.md) | +| `Sinh` | `SinhResult TAMath.Sinh(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the hyperbolic sine for each element in the input array. | [TAMath.Sinh](../functions/TAMath.Sinh.md) | +| `Sqrt` | `SqrtResult TAMath.Sqrt(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the square root for each element in the input array. | [TAMath.Sqrt](../functions/TAMath.Sqrt.md) | +| `Tan` | `TanResult TAMath.Tan(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the trigonometric tangent for each element in the input array. | [TAMath.Tan](../functions/TAMath.Tan.md) | +| `Tanh` | `TanhResult TAMath.Tanh(int startIdx, int endIdx, double[] real)` | _none_ | `Real` | Calculates the hyperbolic tangent for each element in the input array. | [TAMath.Tanh](../functions/TAMath.Tanh.md) | + +## Math Operators + +Element-wise arithmetic and rolling min/max helpers. **11 entry points.** + +| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs | +| --- | --- | --- | --- | --- | --- | +| `Add` | `AddResult TAMath.Add(int startIdx, int endIdx, double[] real0, double[] real1)` | _none_ | `Real` | Performs element-wise addition of two input arrays. | [TAMath.Add](../functions/TAMath.Add.md) | +| `Div` | `DivResult TAMath.Div(int startIdx, int endIdx, double[] real0, double[] real1)` | _none_ | `Real` | Performs element-wise division of two input arrays. | [TAMath.Div](../functions/TAMath.Div.md) | +| `Max` | `MaxResult TAMath.Max(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the highest value over a specified time period (MAX). | [TAMath.Max](../functions/TAMath.Max.md) | +| `MaxIndex` | `MaxIndexResult TAMath.MaxIndex(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Integers` | Calculates the index of the highest value over a specified time period (MAXINDEX). | [TAMath.MaxIndex](../functions/TAMath.MaxIndex.md) | +| `Min` | `MinResult TAMath.Min(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the lowest value over a specified time period (MIN). | [TAMath.Min](../functions/TAMath.Min.md) | +| `MinIndex` | `MinIndexResult TAMath.MinIndex(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Integers` | Calculates the index of the lowest value over a specified time period (MININDEX). | [TAMath.MinIndex](../functions/TAMath.MinIndex.md) | +| `MinMax` | `MinMaxResult TAMath.MinMax(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Min`, `Max` | Calculates both the lowest and highest values over a specified time period (MINMAX). | [TAMath.MinMax](../functions/TAMath.MinMax.md) | +| `MinMaxIndex` | `MinMaxIndexResult TAMath.MinMaxIndex(int startIdx, int endIdx, double[] real, int timePeriod)`
`MinMaxIndexResult TAMath.MinMaxIndex(int startIdx, int endIdx, double[] real)` | `int timePeriod` | `MaxIdx`, `MinIdx` | Calculates the indices of both the lowest and highest values over a specified time period (MINMAXINDEX). | [TAMath.MinMaxIndex](../functions/TAMath.MinMaxIndex.md) | +| `Mult` | `MultResult TAMath.Mult(int startIdx, int endIdx, double[] real0, double[] real1)` | _none_ | `Real` | Performs element-wise multiplication of two input arrays. | [TAMath.Mult](../functions/TAMath.Mult.md) | +| `Sub` | `SubResult TAMath.Sub(int startIdx, int endIdx, double[] real0, double[] real1)` | _none_ | `Real` | Performs element-wise subtraction of two input arrays. | [TAMath.Sub](../functions/TAMath.Sub.md) | +| `Sum` | `SumResult TAMath.Sum(int startIdx, int endIdx, double[] real, int timePeriod = 30)` | `int timePeriod = 30` | `Real` | Calculates the summation of values over a specified time period (SUM). | [TAMath.Sum](../functions/TAMath.Sum.md) | + +## See also + +- [Getting started](../guides/getting-started.md) - installation, `RetCode`/`BegIdx`/`NBElement`, alignment, pitfalls. +- [TradingView integration](../guides/tradingview-integration.md) - Pine Script mapping, parity caveats, UDF feed, webhooks. +- [`Atypical.TechnicalAnalysis.Functions` API reference](../functions/Atypical.TechnicalAnalysis.Functions.md) +- [`Atypical.TechnicalAnalysis.Candles` API reference](../candles/Atypical.TechnicalAnalysis.Candles.md) +- [`Atypical.TechnicalAnalysis.Common` API reference](../common/Atypical.TechnicalAnalysis.Common.md) diff --git a/docs/links b/docs/links index da1657db..d3b5178a 100644 --- a/docs/links +++ b/docs/links @@ -467,6 +467,88 @@ M:TechnicalAnalysis.Functions.ExpResult.#ctor(TechnicalAnalysis.Common.RetCode,S T:TechnicalAnalysis.Functions.ExpResult|ExpResult.md|ExpResult M:TechnicalAnalysis.Functions.FloorResult.#ctor(TechnicalAnalysis.Common.RetCode,System.Int32,System.Int32,System.Double[])|FloorResult.FloorResult(RetCode,int,int,double[]).md|FloorResult(RetCode, int, int, double[]) T:TechnicalAnalysis.Functions.FloorResult|FloorResult.md|FloorResult +P:TechnicalAnalysis.Functions.BollingerBandsSeries.Upper|BollingerBandsSeries.Upper.md|Upper +P:TechnicalAnalysis.Functions.BollingerBandsSeries.Middle|BollingerBandsSeries.Middle.md|Middle +P:TechnicalAnalysis.Functions.BollingerBandsSeries.Lower|BollingerBandsSeries.Lower.md|Lower +M:TechnicalAnalysis.Functions.BollingerBandsSeries.#ctor(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries)|BollingerBandsSeries.BollingerBandsSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md|BollingerBandsSeries(IndicatorSeries, IndicatorSeries, IndicatorSeries) +M:TechnicalAnalysis.Functions.BollingerBandsSeries.AsOf(System.Int32)|BollingerBandsSeries.AsOf(int).md|AsOf(int) +T:TechnicalAnalysis.Functions.BollingerBandsSeries|BollingerBandsSeries.md|BollingerBandsSeries +M:TechnicalAnalysis.Functions.MomentumIndicators.Rsi(TechnicalAnalysis.Functions.PriceSeries,System.Int32)|MomentumIndicators.Rsi(thisPriceSeries,int).md|Rsi(this PriceSeries, int) +M:TechnicalAnalysis.Functions.MomentumIndicators.Adx(TechnicalAnalysis.Functions.PriceSeries,System.Int32)|MomentumIndicators.Adx(thisPriceSeries,int).md|Adx(this PriceSeries, int) +M:TechnicalAnalysis.Functions.MomentumIndicators.Macd(TechnicalAnalysis.Functions.PriceSeries,System.Int32,System.Int32,System.Int32)|MomentumIndicators.Macd(thisPriceSeries,int,int,int).md|Macd(this PriceSeries, int, int, int) +M:TechnicalAnalysis.Functions.MomentumIndicators.Stoch(TechnicalAnalysis.Functions.PriceSeries,System.Int32,System.Int32,TechnicalAnalysis.Common.MAType,System.Int32,TechnicalAnalysis.Common.MAType)|MomentumIndicators.Stoch(thisPriceSeries,int,int,MAType,int,MAType).md|Stoch(this PriceSeries, int, int, MAType, int, MAType) +T:TechnicalAnalysis.Functions.MomentumIndicators|MomentumIndicators.md|MomentumIndicators +M:TechnicalAnalysis.Functions.OverlapStudyIndicators.Sma(TechnicalAnalysis.Functions.PriceSeries,System.Int32)|OverlapStudyIndicators.Sma(thisPriceSeries,int).md|Sma(this PriceSeries, int) +M:TechnicalAnalysis.Functions.OverlapStudyIndicators.Ema(TechnicalAnalysis.Functions.PriceSeries,System.Int32)|OverlapStudyIndicators.Ema(thisPriceSeries,int).md|Ema(this PriceSeries, int) +M:TechnicalAnalysis.Functions.OverlapStudyIndicators.BollingerBands(TechnicalAnalysis.Functions.PriceSeries,System.Int32,System.Double,System.Double,TechnicalAnalysis.Common.MAType)|OverlapStudyIndicators.BollingerBands(thisPriceSeries,int,double,double,MAType).md|BollingerBands(this PriceSeries, int, double, double, MAType) +T:TechnicalAnalysis.Functions.OverlapStudyIndicators|OverlapStudyIndicators.md|OverlapStudyIndicators +M:TechnicalAnalysis.Functions.VolatilityIndicators.Atr(TechnicalAnalysis.Functions.PriceSeries,System.Int32)|VolatilityIndicators.Atr(thisPriceSeries,int).md|Atr(this PriceSeries, int) +T:TechnicalAnalysis.Functions.VolatilityIndicators|VolatilityIndicators.md|VolatilityIndicators +M:TechnicalAnalysis.Functions.VolumeIndicators.Obv(TechnicalAnalysis.Functions.PriceSeries)|VolumeIndicators.Obv(thisPriceSeries).md|Obv(this PriceSeries) +T:TechnicalAnalysis.Functions.VolumeIndicators|VolumeIndicators.md|VolumeIndicators +P:TechnicalAnalysis.Functions.IndicatorSeries.RetCode|IndicatorSeries.RetCode.md|RetCode +P:TechnicalAnalysis.Functions.IndicatorSeries.BarCount|IndicatorSeries.BarCount.md|BarCount +P:TechnicalAnalysis.Functions.IndicatorSeries.WarmCount|IndicatorSeries.WarmCount.md|WarmCount +P:TechnicalAnalysis.Functions.IndicatorSeries.HasValues|IndicatorSeries.HasValues.md|HasValues +P:TechnicalAnalysis.Functions.IndicatorSeries.FirstBar|IndicatorSeries.FirstBar.md|FirstBar +P:TechnicalAnalysis.Functions.IndicatorSeries.LastBar|IndicatorSeries.LastBar.md|LastBar +P:TechnicalAnalysis.Functions.IndicatorSeries.Item(System.Int32)|IndicatorSeries.this[int].md|this[int] +P:TechnicalAnalysis.Functions.IndicatorSeries.Latest|IndicatorSeries.Latest.md|Latest +P:TechnicalAnalysis.Functions.IndicatorSeries.WarmValues|IndicatorSeries.WarmValues.md|WarmValues +M:TechnicalAnalysis.Functions.IndicatorSeries.Empty(System.Int32)|IndicatorSeries.Empty(int).md|Empty(int) +M:TechnicalAnalysis.Functions.IndicatorSeries.Create(TechnicalAnalysis.Common.RetCode,System.Int32,System.Int32,System.Double[],System.Int32)|IndicatorSeries.Create(RetCode,int,int,double[],int).md|Create(RetCode, int, int, double[], int) +M:TechnicalAnalysis.Functions.IndicatorSeries.op_Equality(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries)|IndicatorSeries.operator(IndicatorSeries,IndicatorSeries).md|operator ==(IndicatorSeries, IndicatorSeries) +M:TechnicalAnalysis.Functions.IndicatorSeries.op_Inequality(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries)|IndicatorSeries.operator!(IndicatorSeries,IndicatorSeries).md|operator !=(IndicatorSeries, IndicatorSeries) +M:TechnicalAnalysis.Functions.IndicatorSeries.IsWarmAt(System.Int32)|IndicatorSeries.IsWarmAt(int).md|IsWarmAt(int) +M:TechnicalAnalysis.Functions.IndicatorSeries.AsOf(System.Int32)|IndicatorSeries.AsOf(int).md|AsOf(int) +M:TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(System.Double,System.Int32)|IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(double,int)|CrossedAbove(double, int) +M:TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(System.Double,System.Int32)|IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(double,int)|CrossedBelow(double, int) +M:TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,System.Int32)|IndicatorSeries.CrossedAbove.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove(TechnicalAnalysis.Functions.IndicatorSeries,int)|CrossedAbove(IndicatorSeries, int) +M:TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,System.Int32)|IndicatorSeries.CrossedBelow.md#TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow(TechnicalAnalysis.Functions.IndicatorSeries,int)|CrossedBelow(IndicatorSeries, int) +M:TechnicalAnalysis.Functions.IndicatorSeries.ToBarAlignedArray|IndicatorSeries.ToBarAlignedArray().md|ToBarAlignedArray() +M:TechnicalAnalysis.Functions.IndicatorSeries.ToBarAlignedNullableArray|IndicatorSeries.ToBarAlignedNullableArray().md|ToBarAlignedNullableArray() +M:TechnicalAnalysis.Functions.IndicatorSeries.WarmValuesToArray|IndicatorSeries.WarmValuesToArray().md|WarmValuesToArray() +M:TechnicalAnalysis.Functions.IndicatorSeries.CopyBarAligned(System.Span{System.Double})|IndicatorSeries.CopyBarAligned(Span_double_).md|CopyBarAligned(Span) +M:TechnicalAnalysis.Functions.IndicatorSeries.GetEnumerator|IndicatorSeries.GetEnumerator().md|GetEnumerator() +M:TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries)|IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(TechnicalAnalysis.Functions.IndicatorSeries)|Equals(IndicatorSeries) +M:TechnicalAnalysis.Functions.IndicatorSeries.Equals(System.Object)|IndicatorSeries.Equals.md#TechnicalAnalysis.Functions.IndicatorSeries.Equals(object)|Equals(object) +M:TechnicalAnalysis.Functions.IndicatorSeries.GetHashCode|IndicatorSeries.GetHashCode().md|GetHashCode() +T:TechnicalAnalysis.Functions.IndicatorSeries|IndicatorSeries.md|IndicatorSeries +P:TechnicalAnalysis.Functions.MacdSeries.Line|MacdSeries.Line.md|Line +P:TechnicalAnalysis.Functions.MacdSeries.Signal|MacdSeries.Signal.md|Signal +P:TechnicalAnalysis.Functions.MacdSeries.Histogram|MacdSeries.Histogram.md|Histogram +M:TechnicalAnalysis.Functions.MacdSeries.#ctor(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries)|MacdSeries.MacdSeries(IndicatorSeries,IndicatorSeries,IndicatorSeries).md|MacdSeries(IndicatorSeries, IndicatorSeries, IndicatorSeries) +M:TechnicalAnalysis.Functions.MacdSeries.AsOf(System.Int32)|MacdSeries.AsOf(int).md|AsOf(int) +T:TechnicalAnalysis.Functions.MacdSeries|MacdSeries.md|MacdSeries +P:TechnicalAnalysis.Functions.PriceSeries.Empty|PriceSeries.Empty.md|Empty +P:TechnicalAnalysis.Functions.PriceSeries.BarCount|PriceSeries.BarCount.md|BarCount +P:TechnicalAnalysis.Functions.PriceSeries.IsEmpty|PriceSeries.IsEmpty.md|IsEmpty +P:TechnicalAnalysis.Functions.PriceSeries.HasOpen|PriceSeries.HasOpen.md|HasOpen +P:TechnicalAnalysis.Functions.PriceSeries.HasHighLow|PriceSeries.HasHighLow.md|HasHighLow +P:TechnicalAnalysis.Functions.PriceSeries.HasVolume|PriceSeries.HasVolume.md|HasVolume +P:TechnicalAnalysis.Functions.PriceSeries.Close|PriceSeries.Close.md|Close +P:TechnicalAnalysis.Functions.PriceSeries.Open|PriceSeries.Open.md|Open +P:TechnicalAnalysis.Functions.PriceSeries.High|PriceSeries.High.md|High +P:TechnicalAnalysis.Functions.PriceSeries.Low|PriceSeries.Low.md|Low +P:TechnicalAnalysis.Functions.PriceSeries.Volume|PriceSeries.Volume.md|Volume +M:TechnicalAnalysis.Functions.PriceSeries.FromClose(System.ReadOnlySpan{System.Double})|PriceSeries.FromClose(ReadOnlySpan_double_).md|FromClose(ReadOnlySpan) +M:TechnicalAnalysis.Functions.PriceSeries.FromHlc(System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double})|PriceSeries.FromHlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md|FromHlc(ReadOnlySpan, ReadOnlySpan, ReadOnlySpan) +M:TechnicalAnalysis.Functions.PriceSeries.FromOhlc(System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double})|PriceSeries.FromOhlc(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md|FromOhlc(ReadOnlySpan, ReadOnlySpan, ReadOnlySpan, ReadOnlySpan) +M:TechnicalAnalysis.Functions.PriceSeries.FromOhlcv(System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double},System.ReadOnlySpan{System.Double})|PriceSeries.FromOhlcv(ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_,ReadOnlySpan_double_).md|FromOhlcv(ReadOnlySpan, ReadOnlySpan, ReadOnlySpan, ReadOnlySpan, ReadOnlySpan) +M:TechnicalAnalysis.Functions.PriceSeries.op_Equality(TechnicalAnalysis.Functions.PriceSeries,TechnicalAnalysis.Functions.PriceSeries)|PriceSeries.operator(PriceSeries,PriceSeries).md|operator ==(PriceSeries, PriceSeries) +M:TechnicalAnalysis.Functions.PriceSeries.op_Inequality(TechnicalAnalysis.Functions.PriceSeries,TechnicalAnalysis.Functions.PriceSeries)|PriceSeries.operator!(PriceSeries,PriceSeries).md|operator !=(PriceSeries, PriceSeries) +M:TechnicalAnalysis.Functions.PriceSeries.AsOf(System.Int32)|PriceSeries.AsOf(int).md|AsOf(int) +M:TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult)|PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align(TechnicalAnalysis.Common.SingleOutputResult)|Align(SingleOutputResult) +M:TechnicalAnalysis.Functions.PriceSeries.Align``1(``0,System.Func{``0,System.Double[]})|PriceSeries.Align.md#TechnicalAnalysis.Functions.PriceSeries.Align_TResult_(TResult,System.Func_TResult,double[]_)|Align(TResult, Func) +M:TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries)|PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(TechnicalAnalysis.Functions.PriceSeries)|Equals(PriceSeries) +M:TechnicalAnalysis.Functions.PriceSeries.Equals(System.Object)|PriceSeries.Equals.md#TechnicalAnalysis.Functions.PriceSeries.Equals(object)|Equals(object) +M:TechnicalAnalysis.Functions.PriceSeries.GetHashCode|PriceSeries.GetHashCode().md|GetHashCode() +T:TechnicalAnalysis.Functions.PriceSeries|PriceSeries.md|PriceSeries +P:TechnicalAnalysis.Functions.StochSeries.SlowK|StochSeries.SlowK.md|SlowK +P:TechnicalAnalysis.Functions.StochSeries.SlowD|StochSeries.SlowD.md|SlowD +M:TechnicalAnalysis.Functions.StochSeries.#ctor(TechnicalAnalysis.Functions.IndicatorSeries,TechnicalAnalysis.Functions.IndicatorSeries)|StochSeries.StochSeries(IndicatorSeries,IndicatorSeries).md|StochSeries(IndicatorSeries, IndicatorSeries) +M:TechnicalAnalysis.Functions.StochSeries.AsOf(System.Int32)|StochSeries.AsOf(int).md|AsOf(int) +T:TechnicalAnalysis.Functions.StochSeries|StochSeries.md|StochSeries M:TechnicalAnalysis.Functions.HtDcPeriodResult.#ctor(TechnicalAnalysis.Common.RetCode,System.Int32,System.Int32,System.Double[])|HtDcPeriodResult.HtDcPeriodResult(RetCode,int,int,double[]).md|HtDcPeriodResult(RetCode, int, int, double[]) T:TechnicalAnalysis.Functions.HtDcPeriodResult|HtDcPeriodResult.md|HtDcPeriodResult M:TechnicalAnalysis.Functions.HtDcPhaseResult.#ctor(TechnicalAnalysis.Common.RetCode,System.Int32,System.Int32,System.Double[])|HtDcPhaseResult.HtDcPhaseResult(RetCode,int,int,double[]).md|HtDcPhaseResult(RetCode, int, int, double[]) @@ -635,6 +717,9 @@ M:TechnicalAnalysis.Functions.WmaResult.#ctor(TechnicalAnalysis.Common.RetCode,S T:TechnicalAnalysis.Functions.WmaResult|WmaResult.md|WmaResult M:TechnicalAnalysis.Functions.ZigZagResult.#ctor(TechnicalAnalysis.Common.RetCode,System.Int32,System.Int32,System.Double[])|ZigZagResult.ZigZagResult(RetCode,int,int,double[]).md|ZigZagResult(RetCode, int, int, double[]) T:TechnicalAnalysis.Functions.ZigZagResult|ZigZagResult.md|ZigZagResult +P:TechnicalAnalysis.Functions.IndicatorSeries.Enumerator.Current|IndicatorSeries.Enumerator.Current.md|Current +M:TechnicalAnalysis.Functions.IndicatorSeries.Enumerator.MoveNext|IndicatorSeries.Enumerator.MoveNext().md|MoveNext() +T:TechnicalAnalysis.Functions.IndicatorSeries.Enumerator|IndicatorSeries.Enumerator.md|Enumerator ?:TechnicalAnalysis.Functions.TAMath.Acos|TAMath.Acos.md|Acos ?:TechnicalAnalysis.Functions.TAMath.Ad|TAMath.Ad.md|Ad ?:TechnicalAnalysis.Functions.TAMath.Add|TAMath.Add.md|Add @@ -733,3 +818,8 @@ T:TechnicalAnalysis.Functions.ZigZagResult|ZigZagResult.md|ZigZagResult ?:TechnicalAnalysis.Functions.TAMath.WillR|TAMath.WillR.md|WillR ?:TechnicalAnalysis.Functions.TAMath.Wma|TAMath.Wma.md|Wma ?:TechnicalAnalysis.Functions.TAMath.ZigZag|TAMath.ZigZag.md|ZigZag +?:TechnicalAnalysis.Functions.IndicatorSeries.CrossedAbove|IndicatorSeries.CrossedAbove.md|CrossedAbove +?:TechnicalAnalysis.Functions.IndicatorSeries.CrossedBelow|IndicatorSeries.CrossedBelow.md|CrossedBelow +?:TechnicalAnalysis.Functions.IndicatorSeries.Equals|IndicatorSeries.Equals.md|Equals +?:TechnicalAnalysis.Functions.PriceSeries.Align|PriceSeries.Align.md|Align +?:TechnicalAnalysis.Functions.PriceSeries.Equals|PriceSeries.Equals.md|Equals diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/BacktestSampleRunner.cs b/samples/TechnicalAnalysis.Samples.Backtesting/BacktestSampleRunner.cs new file mode 100644 index 00000000..69140767 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/BacktestSampleRunner.cs @@ -0,0 +1,150 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Text; + +namespace TechnicalAnalysis.Samples.Backtesting; + +/// +/// Runs the sample's whole workload β€” load or generate the data, backtest every strategy plus the baseline, +/// and format the report β€” and returns it as text. +/// +/// +/// Keeping this out of Main means the entire sample is exercisable from a test without touching the +/// console. +/// +public static class BacktestSampleRunner +{ + /// + /// Builds the strategy line-up the sample compares: four indicator strategies followed by the + /// buy-and-hold baseline, which is always last so it reads as the reference column. + /// + /// A fresh list of strategy instances. + public static IReadOnlyList CreateStrategies() + { + return + [ + new SmaCrossoverStrategy(20, 50), + new RsiMeanReversionStrategy(), + new MacdTrendStrategy(), + new BollingerBreakoutStrategy(), + new BuyAndHoldStrategy() + ]; + } + + /// + /// Loads the bar series described by the command line: the CSV file when one was given, otherwise a + /// deterministic synthetic series. + /// + /// The parsed command line. + /// Receives a short human-readable description of the data source. + /// The bar series. + /// is . + /// The CSV file does not exist. + /// The CSV file is malformed. + public static IReadOnlyList LoadBars(CommandLineOptions options, out string description) + { + ArgumentNullException.ThrowIfNull(options); + + if (options.CsvPath is { } path) + { + IReadOnlyList bars = CsvBarLoader.LoadFile(path); + description = string.Format(CultureInfo.InvariantCulture, "CSV file '{0}'", path); + return bars; + } + + description = string.Format( + CultureInfo.InvariantCulture, + "deterministic synthetic series (seed {0})", + options.Seed); + + return SyntheticSeriesGenerator.Generate( + options.BarCount, + options.Seed, + barsPerYear: options.Backtest.BarsPerYear); + } + + /// + /// Runs every strategy over the same series with the same engine settings. + /// + /// The strategies to run. + /// The bar series. + /// The engine settings. + /// One result per strategy, in the order the strategies were supplied. + /// Any argument is . + public static IReadOnlyList RunAll( + IReadOnlyList strategies, + IReadOnlyList bars, + BacktestOptions options) + { + ArgumentNullException.ThrowIfNull(strategies); + ArgumentNullException.ThrowIfNull(bars); + ArgumentNullException.ThrowIfNull(options); + + BacktestEngine engine = new(options); + List results = new(strategies.Count); + + foreach (IStrategy strategy in strategies) + { + results.Add(engine.Run(strategy, bars)); + } + + return results; + } + + /// + /// Produces the complete report the sample prints. + /// + /// The parsed command line. + /// The report text. + /// is . + /// The CSV file does not exist. + /// The CSV file is malformed. + public static string Run(CommandLineOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + IReadOnlyList bars = LoadBars(options, out string dataDescription); + IReadOnlyList problems = CsvBarLoader.Validate(bars); + + StringBuilder builder = new(); + builder.Append(ConsoleReport.RenderConfiguration(options.Backtest, bars.Count, dataDescription)); + + if (problems.Count > 0) + { + builder.Append(Environment.NewLine).Append(" Data warnings:").Append(Environment.NewLine); + foreach (string problem in problems.Take(5)) + { + builder.Append(" - ").Append(problem).Append(Environment.NewLine); + } + } + + if (bars.Count == 0) + { + builder.Append(Environment.NewLine) + .Append("The bar series is empty; there is nothing to backtest.") + .Append(Environment.NewLine); + return builder.ToString(); + } + + IReadOnlyList strategies = CreateStrategies(); + IReadOnlyList results = RunAll(strategies, bars, options.Backtest); + + for (int i = 0; i < results.Count; i++) + { + builder.Append(ConsoleReport.RenderStrategyCard(results[i], strategies[i].Description)); + + if (options.ShowTradeLog) + { + builder.Append(ConsoleReport.RenderTradeLog(results[i])); + } + } + + builder.Append(ConsoleReport.RenderComparison(results)); + + return builder.ToString(); + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/CommandLineOptions.cs b/samples/TechnicalAnalysis.Samples.Backtesting/CommandLineOptions.cs new file mode 100644 index 00000000..7a794282 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/CommandLineOptions.cs @@ -0,0 +1,208 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting; + +/// +/// The parsed command line of the sample. +/// +public sealed record CommandLineOptions +{ + /// + /// The usage text printed by --help and after a parse error. + /// + public const string Usage = """ + TaLibStandard - backtesting sample + + Runs four indicator strategies and a buy-and-hold baseline over the same price series, + then prints a side-by-side performance comparison. With no arguments it uses a deterministic + synthetic series, so it works completely offline. + + Usage: + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting [options] + + Options: + --csv Load bars from a CSV file instead of generating them. + Header required: Date,Open,High,Low,Close,Volume (any order). + --bars Number of synthetic bars to generate. Default: 1500. + --seed Seed of the synthetic series generator. Default: 20240101. + --capital Starting account equity. Default: 100000. + --commission-bps Commission per fill, in basis points of notional. Default: 5. + --slippage-bps Slippage per fill, in basis points of price. Default: 2. + --bars-per-year Annualisation constant for CAGR, volatility, Sharpe, Sortino, + Calmar. Default: 252 (daily bars, equity calendar). + --allow-short Permit short positions. Off by default, in which case a short + signal simply flattens the position. + --trade-log Print an excerpt of each strategy's round trips. + -h, --help Print this text and exit. + + Examples: + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -- --seed 7 --bars 3000 --allow-short + dotnet run --project samples/TechnicalAnalysis.Samples.Backtesting -- --csv ./spy.csv --commission-bps 10 + """; + + /// + /// Gets a value indicating whether the user asked for the usage text. + /// + public bool ShowHelp { get; init; } + + /// + /// Gets the path of the CSV file to load, or to generate a synthetic series. + /// + public string? CsvPath { get; init; } + + /// + /// Gets the number of synthetic bars to generate when no CSV file is supplied. Defaults to 1500. + /// + public int BarCount { get; init; } = 1_500; + + /// + /// Gets the seed of the synthetic series generator. + /// + public int Seed { get; init; } = SyntheticSeriesGenerator.DefaultSeed; + + /// + /// Gets a value indicating whether an excerpt of the trade log should be printed for each strategy. + /// + public bool ShowTradeLog { get; init; } + + /// + /// Gets the backtest options assembled from the command line. + /// + public BacktestOptions Backtest { get; init; } = new(); + + /// + /// Parses a command line. + /// + /// The raw arguments, as handed to Main. + /// The parsed options. + /// is . + /// An option is unknown, missing its value, or its value is not parsable or out of range. + public static CommandLineOptions Parse(IReadOnlyList args) + { + ArgumentNullException.ThrowIfNull(args); + + CommandLineOptions options = new(); + BacktestOptions backtest = options.Backtest; + + for (int i = 0; i < args.Count; i++) + { + string argument = args[i]; + switch (argument) + { + case "-h": + case "--help": + return options with { ShowHelp = true }; + + case "--csv": + options = options with { CsvPath = NextValue(args, ref i) }; + break; + + case "--bars": + options = options with { BarCount = ParseInt(NextValue(args, ref i), argument, 1) }; + break; + + case "--seed": + options = options with { Seed = ParseInt(NextValue(args, ref i), argument, int.MinValue) }; + break; + + case "--capital": + backtest = backtest with { InitialCapital = ParseDouble(NextValue(args, ref i), argument) }; + break; + + case "--commission-bps": + backtest = backtest with { CommissionBps = ParseDouble(NextValue(args, ref i), argument) }; + break; + + case "--slippage-bps": + backtest = backtest with { SlippageBps = ParseDouble(NextValue(args, ref i), argument) }; + break; + + case "--bars-per-year": + backtest = backtest with { BarsPerYear = ParseInt(NextValue(args, ref i), argument, 1) }; + break; + + case "--allow-short": + backtest = backtest with { AllowShort = true }; + break; + + case "--trade-log": + options = options with { ShowTradeLog = true }; + break; + + default: + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Unknown option '{0}'.", + argument)); + } + } + + try + { + backtest.Validate(); + } + catch (ArgumentException ex) + { + throw new FormatException(ex.Message, ex); + } + + return options with { Backtest = backtest }; + } + + private static string NextValue(IReadOnlyList args, ref int index) + { + if (index + 1 >= args.Count) + { + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Option '{0}' requires a value.", + args[index])); + } + + index++; + return args[index]; + } + + private static int ParseInt(string text, string option, int minimum) + { + if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value)) + { + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Option '{0}' expects an integer but got '{1}'.", + option, + text)); + } + + if (value < minimum) + { + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Option '{0}' expects a value of at least {1} but got {2}.", + option, + minimum, + value)); + } + + return value; + } + + private static double ParseDouble(string text, string option) + { + if (!double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) + { + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Option '{0}' expects a number but got '{1}'.", + option, + text)); + } + + return value; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Data/CsvBarLoader.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Data/CsvBarLoader.cs new file mode 100644 index 00000000..bd171e2f --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Data/CsvBarLoader.cs @@ -0,0 +1,271 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Data; + +/// +/// Loads an OHLCV bar series from a comma-separated file, so the sample can be pointed at real data. +/// +/// +/// +/// The file must start with a header row naming the columns. Recognised names, matched case-insensitively, +/// are Date (or Timestamp, Time, DateTime), Open, High, Low, +/// Close and Volume. Column order does not matter and extra columns are ignored, so an +/// export carrying an Adj Close column loads unchanged. Volume is optional and defaults to zero. +/// +/// +/// Numbers and dates are parsed with β€” a decimal comma will be +/// rejected rather than silently misread. Blank lines are skipped. Rows are returned in file order and are +/// not re-sorted: an unsorted file is a data problem the caller should see, and +/// reports it. +/// +/// +public static class CsvBarLoader +{ + private static readonly string[] DateHeaders = ["date", "timestamp", "time", "datetime"]; + + /// + /// Loads a bar series from a CSV file on disk. + /// + /// The path of the file to read. + /// The parsed bars, in file order. + /// is , empty or blank. + /// The file does not exist. + /// The header is missing a required column, or a row is malformed. + public static IReadOnlyList LoadFile(string path) + { + ArgumentException.ThrowIfNullOrWhiteSpace(path); + + if (!File.Exists(path)) + { + throw new FileNotFoundException( + string.Format(CultureInfo.InvariantCulture, "CSV file '{0}' was not found.", path), + path); + } + + using StreamReader reader = new(path); + return Load(reader); + } + + /// + /// Loads a bar series from a reader. Useful for tests and for streaming from a non-file source. + /// + /// The reader positioned at the header row. + /// The parsed bars, in reader order. + /// is . + /// The header is missing or malformed, or a row is malformed. + public static IReadOnlyList Load(TextReader reader) + { + ArgumentNullException.ThrowIfNull(reader); + + string? headerLine = ReadNonEmptyLine(reader, out int lineNumber); + if (headerLine is null) + { + throw new FormatException("The CSV source is empty; a header row naming the columns is required."); + } + + ColumnMap columns = ColumnMap.FromHeader(headerLine); + List bars = []; + + while (true) + { + string? line = reader.ReadLine(); + lineNumber++; + + if (line is null) + { + break; + } + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + bars.Add(ParseRow(line, lineNumber, columns)); + } + + return bars; + } + + /// + /// Checks a loaded series for the two problems that quietly break a backtest: bars that are not ordered + /// strictly ascending in time, and bars whose prices are inconsistent. + /// + /// The series to check. + /// A human-readable description of the problems found, empty when the series is clean. + /// is . + public static IReadOnlyList Validate(IReadOnlyList bars) + { + ArgumentNullException.ThrowIfNull(bars); + + List problems = []; + for (int i = 0; i < bars.Count; i++) + { + if (!bars[i].IsWellFormed()) + { + problems.Add(string.Format( + CultureInfo.InvariantCulture, + "Bar {0} ({1:yyyy-MM-dd}) is not well formed: O={2} H={3} L={4} C={5} V={6}.", + i, + bars[i].Timestamp, + bars[i].Open, + bars[i].High, + bars[i].Low, + bars[i].Close, + bars[i].Volume)); + } + + if (i > 0 && bars[i].Timestamp <= bars[i - 1].Timestamp) + { + problems.Add(string.Format( + CultureInfo.InvariantCulture, + "Bar {0} ({1:yyyy-MM-dd HH:mm:ss}) is not strictly after bar {2} ({3:yyyy-MM-dd HH:mm:ss}).", + i, + bars[i].Timestamp, + i - 1, + bars[i - 1].Timestamp)); + } + } + + return problems; + } + + private static string? ReadNonEmptyLine(TextReader reader, out int lineNumber) + { + lineNumber = 0; + while (true) + { + string? line = reader.ReadLine(); + lineNumber++; + + if (line is null) + { + return null; + } + + if (!string.IsNullOrWhiteSpace(line)) + { + return line; + } + } + } + + private static Bar ParseRow(string line, int lineNumber, ColumnMap columns) + { + string[] fields = line.Split(','); + if (fields.Length < columns.RequiredFieldCount) + { + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Line {0}: expected at least {1} comma-separated fields but found {2}.", + lineNumber, + columns.RequiredFieldCount, + fields.Length)); + } + + DateTime timestamp = ParseDate(fields[columns.Date], lineNumber, "date"); + double open = ParseDouble(fields[columns.Open], lineNumber, "open"); + double high = ParseDouble(fields[columns.High], lineNumber, "high"); + double low = ParseDouble(fields[columns.Low], lineNumber, "low"); + double close = ParseDouble(fields[columns.Close], lineNumber, "close"); + double volume = columns.Volume >= 0 && columns.Volume < fields.Length + ? ParseDouble(fields[columns.Volume], lineNumber, "volume") + : 0.0; + + return new Bar(timestamp, open, high, low, close, volume); + } + + private static DateTime ParseDate(string field, int lineNumber, string column) + { + string text = field.Trim().Trim('"'); + const DateTimeStyles styles = DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal; + if (DateTime.TryParse(text, CultureInfo.InvariantCulture, styles, out DateTime value)) + { + return value; + } + + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Line {0}: '{1}' is not a valid invariant-culture {2}.", + lineNumber, + text, + column)); + } + + private static double ParseDouble(string field, int lineNumber, string column) + { + string text = field.Trim().Trim('"'); + if (double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double value)) + { + return value; + } + + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "Line {0}: '{1}' is not a valid invariant-culture {2} value.", + lineNumber, + text, + column)); + } + + private sealed record ColumnMap(int Date, int Open, int High, int Low, int Close, int Volume) + { + public int RequiredFieldCount => Math.Max(Math.Max(Date, Open), Math.Max(Math.Max(High, Low), Close)) + 1; + + public static ColumnMap FromHeader(string headerLine) + { + string[] headers = headerLine.Split(','); + Dictionary index = new(StringComparer.OrdinalIgnoreCase); + + for (int i = 0; i < headers.Length; i++) + { + string name = headers[i].Trim().Trim('"'); + if (name.Length > 0) + { + index.TryAdd(name, i); + } + } + + int date = -1; + foreach (string candidate in DateHeaders) + { + if (index.TryGetValue(candidate, out int found)) + { + date = found; + break; + } + } + + if (date < 0) + { + throw new FormatException( + "The CSV header must contain a date column named 'Date', 'Timestamp', 'Time' or 'DateTime'."); + } + + return new ColumnMap( + date, + Required(index, "Open"), + Required(index, "High"), + Required(index, "Low"), + Required(index, "Close"), + index.TryGetValue("Volume", out int volume) ? volume : -1); + } + + private static int Required(Dictionary index, string name) + { + if (index.TryGetValue(name, out int position)) + { + return position; + } + + throw new FormatException(string.Format( + CultureInfo.InvariantCulture, + "The CSV header must contain a '{0}' column.", + name)); + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Data/DeterministicRandom.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Data/DeterministicRandom.cs new file mode 100644 index 00000000..732d66d2 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Data/DeterministicRandom.cs @@ -0,0 +1,91 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Data; + +/// +/// A tiny SplitMix64 pseudo-random generator, used to make the sample's synthetic data byte-for-byte +/// reproducible. +/// +/// +/// is deliberately avoided: its sequence is an implementation detail of the +/// runtime, so a sample that relied on it could print different numbers on a different .NET version and +/// silently invalidate every documented figure. SplitMix64 is eight lines long, has a documented reference +/// implementation and is entirely fixed by its seed. It is a statistical generator, never a cryptographic +/// one β€” do not use it for anything that needs to be unpredictable. +/// +public sealed class DeterministicRandom +{ + private const ulong GoldenGamma = 0x9E3779B97F4A7C15UL; + + private ulong _state; + private double _spareGaussian; + private bool _hasSpareGaussian; + + /// + /// Initializes a new instance of the class. + /// + /// The seed. The same seed always yields the same sequence, on every platform and runtime. + public DeterministicRandom(int seed) + { + _state = unchecked((ulong)seed); + } + + /// + /// Returns the next 64 raw pseudo-random bits. + /// + /// A uniformly distributed 64-bit value. + public ulong NextUInt64() + { + unchecked + { + _state += GoldenGamma; + ulong z = _state; + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9UL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBUL; + return z ^ (z >> 31); + } + } + + /// + /// Returns the next uniformly distributed value in the half-open interval [0, 1). + /// + /// A uniform deviate in [0, 1). + public double NextDouble() + { + // 53 significant bits is exactly the mantissa of a double, so every representable value is reachable + // and none is favoured. + return (NextUInt64() >> 11) * (1.0 / 9007199254740992.0); + } + + /// + /// Returns the next standard normal deviate, using the polar form of the Box-Muller transform. + /// + /// A deviate drawn from N(0, 1). + public double NextGaussian() + { + if (_hasSpareGaussian) + { + _hasSpareGaussian = false; + return _spareGaussian; + } + + double u1; + do + { + u1 = NextDouble(); + } + while (u1 <= double.Epsilon); + + double u2 = NextDouble(); + double magnitude = Math.Sqrt(-2.0 * Math.Log(u1)); + + _spareGaussian = magnitude * Math.Sin(2.0 * Math.PI * u2); + _hasSpareGaussian = true; + + return magnitude * Math.Cos(2.0 * Math.PI * u2); + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Data/SyntheticSeriesGenerator.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Data/SyntheticSeriesGenerator.cs new file mode 100644 index 00000000..a900b2a2 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Data/SyntheticSeriesGenerator.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Data; + +/// +/// Generates a deterministic synthetic OHLCV series so the sample runs offline, with no market-data feed, +/// no API key and no network access, and prints the same numbers on every machine. +/// +/// +/// +/// Closes follow a geometric Brownian motion, +/// C(t) = C(t-1) * exp((mu - sigma^2 / 2) + sigma * z), with a mild positive drift so the series +/// trends upwards over the long run β€” trend followers need trends to be worth comparing β€” while remaining +/// noisy enough at short horizons for mean-reversion rules to find something to do. +/// +/// +/// The open gaps away from the previous close, and the high and low are pushed beyond the open/close range by +/// half-normal shocks, so the bars are well formed (Low <= min(Open, Close) and +/// High >= max(Open, Close)) and have a realistic true range for ATR-based rules. Timestamps step +/// one weekday at a time, matching the default annualisation constant of 252 bars per year. +/// +/// +/// This is synthetic data, not market data. It has no fat tails, no volatility clustering and no regime +/// changes, so it is fine for exercising and testing an engine and hopeless as evidence that a strategy works. +/// +/// +public static class SyntheticSeriesGenerator +{ + /// + /// The default seed used by the sample. Chosen only for readability of the output. + /// + public const int DefaultSeed = 20_240_101; + + private static readonly DateTime DefaultStartDate = new(2015, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Generates a synthetic OHLCV series. + /// + /// The number of bars to generate. Zero produces an empty series. + /// The PRNG seed. Identical seeds produce identical series. Defaults to . + /// The price the first bar opens at. Defaults to 100. + /// The annual drift of the log price, as a decimal fraction. Defaults to 0.12 (12% a year). + /// + /// The annual volatility of the log price, as a decimal fraction. Defaults to 0.22 (22% a year). + /// + /// + /// The number of bars in a year, used to de-annualise the drift and volatility. Defaults to 252. + /// + /// The timestamp of the first bar. Defaults to 2015-01-01 UTC. + /// A well-formed bar series of length . + /// An argument is negative, non-finite or otherwise out of range. + public static IReadOnlyList Generate( + int barCount, + int seed = DefaultSeed, + double startPrice = 100.0, + double annualDrift = 0.12, + double annualVolatility = 0.22, + int barsPerYear = 252, + DateTime? startDate = null) + { + ArgumentOutOfRangeException.ThrowIfNegative(barCount); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(startPrice); + ArgumentOutOfRangeException.ThrowIfNegative(annualVolatility); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(barsPerYear); + + if (barCount == 0) + { + return []; + } + + DeterministicRandom random = new(seed); + double drift = annualDrift / barsPerYear; + double volatility = annualVolatility / Math.Sqrt(barsPerYear); + double gapVolatility = volatility * 0.30; + double wickVolatility = volatility * 0.55; + + List bars = new(barCount); + DateTime timestamp = startDate ?? DefaultStartDate; + double previousClose = startPrice; + + for (int i = 0; i < barCount; i++) + { + double open = i == 0 + ? startPrice + : previousClose * Math.Exp(gapVolatility * random.NextGaussian()); + + double close = open * Math.Exp(drift - (0.5 * volatility * volatility) + (volatility * random.NextGaussian())); + + double bodyHigh = Math.Max(open, close); + double bodyLow = Math.Min(open, close); + double high = bodyHigh * Math.Exp(Math.Abs(random.NextGaussian()) * wickVolatility); + double low = bodyLow * Math.Exp(-Math.Abs(random.NextGaussian()) * wickVolatility); + double volume = Math.Round(1_000_000.0 * Math.Exp(0.35 * random.NextGaussian())); + + bars.Add(new Bar(timestamp, open, high, low, close, volume)); + + previousClose = close; + timestamp = NextWeekday(timestamp); + } + + return bars; + } + + private static DateTime NextWeekday(DateTime timestamp) + { + DateTime next = timestamp.AddDays(1); + while (next.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday) + { + next = next.AddDays(1); + } + + return next; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestEngine.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestEngine.cs new file mode 100644 index 00000000..3f26caf2 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestEngine.cs @@ -0,0 +1,310 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// A single-instrument, bar-by-bar backtester with an explicitly causal execution model. +/// +/// +/// +/// The execution timeline. For every bar i, in this exact order: +/// +/// +/// +/// +/// the cursor moves to bar i, so bars 0..i become readable and i+1.. do not; +/// +/// +/// +/// +/// the signal produced at the close of bar i-1 is filled at Open(i), with slippage and +/// commission; +/// +/// +/// +/// +/// on the final bar, any position still open is liquidated at Close(i) when +/// is set; +/// +/// +/// +/// +/// the account is marked to market at Close(i) and one is appended; +/// +/// +/// +/// +/// the strategy is asked for a signal, seeing bars 0..i only. That signal is queued for bar +/// i+1. +/// +/// +/// +/// +/// Why there is no look-ahead bias. The rule "a decision taken from bars 0..i executes at +/// Open(i+1)" is enforced by construction, not by discipline: +/// +/// +/// +/// +/// the strategy is handed an whose is i+1 and +/// which throws for any index above i; +/// +/// +/// +/// +/// indicators are pre-computed over the whole series (which is safe, because every TA-Lib function is +/// causal) and then re-indexed through , which is bound to the same cursor and +/// throws on a future index; +/// +/// +/// +/// +/// the length of the full series never leaks either: , +/// and are window-relative too, +/// so a strategy cannot read the total bar count out of an indicator it obtained in +/// ; +/// +/// +/// +/// +/// nothing in , or +/// returns the backing arrays, so the future cannot be reached indirectly; +/// +/// +/// +/// +/// the returned signal is stored, not executed, and the fill price is read from the next bar's +/// open. +/// +/// +/// +/// +/// A strategy that tries to cheat therefore fails loudly with rather than +/// quietly reporting an impossible Sharpe ratio. The engine never swallows that exception. +/// +/// +/// Cost model. A buy fills at price * (1 + slippage), a sell at price * (1 - slippage), +/// and each fill is charged notional * commission where the notional uses the filled price. Position +/// size is chosen so that the cash needed β€” notional plus commission β€” never exceeds the allocated budget, +/// which keeps cash non-negative for any up to 1. +/// +/// +public sealed class BacktestEngine +{ + /// + /// Initializes a new instance of the class with the default option set. + /// + public BacktestEngine() + : this(new BacktestOptions()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The simulated market and account settings. + /// is . + /// contains an out-of-range value. + public BacktestEngine(BacktestOptions options) + { + ArgumentNullException.ThrowIfNull(options); + options.Validate(); + Options = options; + } + + /// + /// Gets the options this engine simulates with. + /// + public BacktestOptions Options { get; } + + /// + /// Runs a strategy over a bar series. + /// + /// The strategy to simulate. + /// + /// The bar series, ordered ascending by timestamp. An empty series is valid and yields an empty result. + /// + /// The equity curve, the completed trades and the performance metrics of the run. + /// + /// or is . + /// + /// The strategy attempted to read data from the future. + public BacktestResult Run(IStrategy strategy, IReadOnlyList bars) + { + ArgumentNullException.ThrowIfNull(strategy); + ArgumentNullException.ThrowIfNull(bars); + + BarWindow window = new(bars); + IndicatorSet indicators = new(bars, window); + strategy.Initialize(indicators); + + RunState state = new(Options.InitialCapital); + Signal pending = Signal.Hold; + int lastIndex = bars.Count - 1; + + for (int i = 0; i <= lastIndex; i++) + { + // (1) Advance the causal cursor. Bars 0..i are now readable; i+1.. still throw. + window.MoveTo(i); + Bar bar = bars[i]; + + // (2) Fill the order decided at the close of bar i-1, at the open of bar i. + if (pending != Signal.Hold) + { + Apply(pending, state, i, bar.Timestamp, bar.Open); + } + + // (3) Liquidate on the last bar so the trade list and the final equity are fully realised. + if (i == lastIndex && state.Position is not null && Options.CloseOpenPositionAtEnd) + { + ClosePosition(state, i, bar.Timestamp, bar.Close); + } + + // (4) Mark to market at the close of bar i. + double signedQuantity = state.Position?.SignedQuantity ?? 0.0; + double equity = state.Cash + (signedQuantity * bar.Close); + state.EquityCurve.Add(new EquityPoint(i, bar.Timestamp, bar.Close, state.Cash, signedQuantity, equity)); + + // (5) Ask for the next target exposure. The strategy sees bars 0..i and nothing else. + // On the last bar the answer can no longer be executed, so it is discarded. + Signal next = strategy.Evaluate(window, state.Position); + pending = i < lastIndex ? next : Signal.Hold; + } + + PerformanceMetrics metrics = PerformanceMetrics.Compute( + state.EquityCurve, + state.Trades, + Options.InitialCapital, + Options.BarsPerYear, + Options.RiskFreeRate); + + return new BacktestResult(strategy.Name, Options, state.EquityCurve, state.Trades, metrics); + } + + private void Apply(Signal signal, RunState state, int index, DateTime timestamp, double price) + { + // Shorting disabled: a reversal signal degrades to "go flat" rather than being silently ignored, + // so a long/short strategy still behaves sensibly in a long-only account. + if (signal == Signal.EnterShort && !Options.AllowShort) + { + signal = Signal.Exit; + } + + switch (signal) + { + case Signal.Exit: + ClosePosition(state, index, timestamp, price); + break; + + case Signal.EnterLong: + if (state.Position?.IsLong == true) + { + return; + } + + ClosePosition(state, index, timestamp, price); + OpenPosition(state, OrderSide.Buy, index, timestamp, price); + break; + + case Signal.EnterShort: + if (state.Position?.IsShort == true) + { + return; + } + + ClosePosition(state, index, timestamp, price); + OpenPosition(state, OrderSide.Sell, index, timestamp, price); + break; + + case Signal.Hold: + default: + break; + } + } + + private void OpenPosition(RunState state, OrderSide side, int index, DateTime timestamp, double price) + { + double fillPrice = FillPrice(price, side); + if (!double.IsFinite(fillPrice) || fillPrice <= 0.0) + { + return; + } + + // Any previous position has already been liquidated by the caller, so cash is the whole of equity. + double equity = state.Cash + (state.Position?.MarketValue(price) ?? 0.0); + double budget = Options.Sizing == PositionSizing.FixedFraction + ? equity * Options.PositionFraction + : Math.Min(Options.PositionCash, equity); + + if (!double.IsFinite(budget) || budget <= 0.0) + { + return; + } + + // Solve qty * fillPrice * (1 + commissionRate) = budget so that the cash leg, commission included, + // exactly consumes the budget and never overdraws the account. + double quantity = budget / (fillPrice * (1.0 + Options.CommissionRate)); + if (!double.IsFinite(quantity) || quantity <= 0.0) + { + return; + } + + double commission = quantity * fillPrice * Options.CommissionRate; + state.Cash += side == OrderSide.Buy + ? -((quantity * fillPrice) + commission) + : (quantity * fillPrice) - commission; + + state.Position = new Position(side, quantity, fillPrice, index, timestamp, commission); + } + + private void ClosePosition(RunState state, int index, DateTime timestamp, double price) + { + if (state.Position is not { } position) + { + return; + } + + OrderSide closingSide = position.IsLong ? OrderSide.Sell : OrderSide.Buy; + double fillPrice = FillPrice(price, closingSide); + double commission = position.Quantity * fillPrice * Options.CommissionRate; + + state.Cash += position.IsLong + ? (position.Quantity * fillPrice) - commission + : -((position.Quantity * fillPrice) + commission); + + state.Trades.Add(new Trade( + position.Side, + position.Quantity, + position.EntryIndex, + position.EntryTime, + position.EntryPrice, + index, + timestamp, + fillPrice, + position.EntryCommission + commission)); + + state.Position = null; + } + + private double FillPrice(double price, OrderSide side) + { + return side == OrderSide.Buy + ? price * (1.0 + Options.SlippageRate) + : price * (1.0 - Options.SlippageRate); + } + + private sealed class RunState(double initialCash) + { + public double Cash { get; set; } = initialCash; + + public Position? Position { get; set; } + + public List Trades { get; } = []; + + public List EquityCurve { get; } = []; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestOptions.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestOptions.cs new file mode 100644 index 00000000..cd43505a --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestOptions.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// Everything that describes the simulated market and account: starting capital, trading frictions, position +/// sizing policy and the annualisation constant used by the performance metrics. +/// +public sealed record BacktestOptions +{ + /// + /// Gets the cash balance the account starts with, in account currency. Defaults to 100 000. + /// + public double InitialCapital { get; init; } = 100_000.0; + + /// + /// Gets the commission charged on every fill, in basis points of the traded notional + /// (1 bp = 0.01% = 0.0001). A round trip therefore pays it twice. Defaults to 5 bp. + /// + public double CommissionBps { get; init; } = 5.0; + + /// + /// Gets the slippage applied to every fill, in basis points of the quoted price + /// (1 bp = 0.01% = 0.0001). Buys fill at price * (1 + SlippageBps / 10000) and sells at + /// price * (1 - SlippageBps / 10000), so slippage always works against the account. + /// Defaults to 2 bp. + /// + public double SlippageBps { get; init; } = 2.0; + + /// + /// Gets the policy used to turn a signal into a quantity. Defaults to + /// . + /// + public PositionSizing Sizing { get; init; } = PositionSizing.FixedFraction; + + /// + /// Gets the fraction of current account equity committed to each new position when + /// is . Must lie in (0, 1]. + /// Defaults to 1.0 (fully invested, no leverage). + /// + public double PositionFraction { get; init; } = 1.0; + + /// + /// Gets the cash notional committed to each new position when is + /// . It is capped by the current account equity, so the account + /// can never take on leverage. Defaults to 10 000. + /// + public double PositionCash { get; init; } = 10_000.0; + + /// + /// Gets a value indicating whether short positions are permitted. When the engine + /// downgrades to , so a strategy designed to + /// reverse simply goes flat. Defaults to . + /// + public bool AllowShort { get; init; } + + /// + /// Gets a value indicating whether a position still open on the last bar is liquidated at that bar's + /// close, so that the trade list is complete and the final equity is fully realised. Defaults to + /// . + /// + public bool CloseOpenPositionAtEnd { get; init; } = true; + + /// + /// Gets the number of bars in one year. This is the annualisation constant used by every annualised + /// metric β€” CAGR, volatility, Sharpe, Sortino and Calmar β€” and it must match the bar interval of the + /// data: 252 for daily bars on an equity calendar, 365 for daily crypto bars, + /// 52 for weekly, 12 for monthly, 98 280 for one-minute equity bars. + /// Defaults to 252. + /// + public int BarsPerYear { get; init; } = 252; + + /// + /// Gets the annual risk-free rate used as the benchmark in the Sharpe and Sortino ratios, expressed as a + /// decimal fraction (0.04 is 4% a year). It is de-annualised geometrically to a per-bar rate: + /// (1 + rate) ^ (1 / BarsPerYear) - 1. Defaults to 0. + /// + public double RiskFreeRate { get; init; } + + /// + /// Gets the commission expressed as a decimal fraction of notional, that is CommissionBps / 10000. + /// + public double CommissionRate => CommissionBps / 10_000.0; + + /// + /// Gets the slippage expressed as a decimal fraction of price, that is SlippageBps / 10000. + /// + public double SlippageRate => SlippageBps / 10_000.0; + + /// + /// Throws when the option set is not usable for a simulation. + /// + /// A value is out of range or not finite. + public void Validate() + { + Require(double.IsFinite(InitialCapital) && InitialCapital > 0.0, nameof(InitialCapital), "must be finite and strictly positive."); + Require(double.IsFinite(CommissionBps) && CommissionBps >= 0.0, nameof(CommissionBps), "must be finite and non-negative."); + Require(double.IsFinite(SlippageBps) && SlippageBps >= 0.0, nameof(SlippageBps), "must be finite and non-negative."); + Require(SlippageRate < 1.0, nameof(SlippageBps), "must be below 10000 bp, otherwise a sell would fill at or below zero."); + Require(double.IsFinite(PositionFraction) && PositionFraction is > 0.0 and <= 1.0, nameof(PositionFraction), "must lie in (0, 1]."); + Require(double.IsFinite(PositionCash) && PositionCash > 0.0, nameof(PositionCash), "must be finite and strictly positive."); + Require(BarsPerYear > 0, nameof(BarsPerYear), "must be strictly positive."); + Require(double.IsFinite(RiskFreeRate) && RiskFreeRate > -1.0, nameof(RiskFreeRate), "must be finite and greater than -1."); + } + + private static void Require(bool condition, string name, string requirement) + { + if (!condition) + { + throw new ArgumentException( + string.Format(CultureInfo.InvariantCulture, "BacktestOptions.{0} {1}", name, requirement)); + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestResult.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestResult.cs new file mode 100644 index 00000000..d6301abf --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BacktestResult.cs @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The complete outcome of one simulation: the equity curve sampled at every bar close, the list of completed +/// round trips, and the performance metrics derived from both. +/// +public sealed record BacktestResult +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the strategy that produced the result. + /// The options the simulation ran under. + /// The equity curve, one point per bar of the source series. + /// The completed round trips, in chronological order. + /// The performance metrics derived from the curve and the trades. + /// Any argument is . + public BacktestResult( + string strategyName, + BacktestOptions options, + IReadOnlyList equityCurve, + IReadOnlyList trades, + PerformanceMetrics metrics) + { + ArgumentNullException.ThrowIfNull(strategyName); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(equityCurve); + ArgumentNullException.ThrowIfNull(trades); + ArgumentNullException.ThrowIfNull(metrics); + + StrategyName = strategyName; + Options = options; + EquityCurve = equityCurve; + Trades = trades; + Metrics = metrics; + } + + /// + /// Gets the name of the strategy that produced the result. + /// + public string StrategyName { get; } + + /// + /// Gets the options the simulation ran under. + /// + public BacktestOptions Options { get; } + + /// + /// Gets the equity curve, sampled at the close of every bar after that bar's fills. + /// + public IReadOnlyList EquityCurve { get; } + + /// + /// Gets the completed round trips, in chronological order of their exit. + /// + public IReadOnlyList Trades { get; } + + /// + /// Gets the performance metrics of the run. + /// + public PerformanceMetrics Metrics { get; } + + /// + /// Gets the starting equity of the account. + /// + public double InitialCapital => Options.InitialCapital; + + /// + /// Gets the equity at the close of the last bar, or the initial capital when the series was empty. + /// + public double FinalEquity => EquityCurve.Count > 0 ? EquityCurve[^1].Equity : Options.InitialCapital; +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Bar.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Bar.cs new file mode 100644 index 00000000..762d56da --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Bar.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// Represents a single OHLCV price bar (candle) of a time series. +/// +/// The instant the bar closed. Bars are expected to be ordered strictly ascending by this value. +/// The first traded price of the bar. Orders generated on the previous bar are filled at this price. +/// The highest traded price of the bar. +/// The lowest traded price of the bar. +/// The last traded price of the bar. Used for mark-to-market valuation of open positions. +/// The traded volume of the bar. +public sealed record Bar(DateTime Timestamp, double Open, double High, double Low, double Close, double Volume) +{ + /// + /// Gets the typical price of the bar, defined as (High + Low + Close) / 3. + /// + public double TypicalPrice => (High + Low + Close) / 3.0; + + /// + /// Gets the high-low range of the bar, defined as High - Low. + /// + public double Range => High - Low; + + /// + /// Validates that the bar is internally consistent: the high is the maximum and the low is the minimum + /// of the open, high, low and close prices, all prices are finite and strictly positive, and the volume + /// is finite and non-negative. + /// + /// when the bar is internally consistent; otherwise . + public bool IsWellFormed() + { + if (!double.IsFinite(Open) || !double.IsFinite(High) || !double.IsFinite(Low) || !double.IsFinite(Close)) + { + return false; + } + + if (Open <= 0.0 || High <= 0.0 || Low <= 0.0 || Close <= 0.0) + { + return false; + } + + if (!double.IsFinite(Volume) || Volume < 0.0) + { + return false; + } + + return High >= Math.Max(Open, Close) && Low <= Math.Min(Open, Close) && High >= Low; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BarWindow.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BarWindow.cs new file mode 100644 index 00000000..3c644c52 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BarWindow.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The default : a movable cursor over a full bar series that refuses every read +/// beyond its current position. +/// +/// +/// The cursor is advanced by only β€” is internal, and the +/// backing series is private with no accessor, so strategy code holding an can +/// neither move the cursor forward nor reach around it. The public constructor exists so that the guard +/// itself can be unit tested at an arbitrary position. +/// +public sealed class BarWindow : IBarWindow +{ + private readonly IReadOnlyList _bars; + + /// + /// Initializes a new instance of the class positioned before the first bar, + /// with equal to -1. + /// + /// The full bar series. Never exposed to callers. + /// is . + public BarWindow(IReadOnlyList bars) + : this(bars, -1) + { + } + + /// + /// Initializes a new instance of the class positioned on a specific bar. + /// + /// The full bar series. Never exposed to callers. + /// The initial cursor position. Use -1 for "before the first bar". + /// is . + /// is below -1 or past the last bar. + public BarWindow(IReadOnlyList bars, int currentIndex) + { + ArgumentNullException.ThrowIfNull(bars); + ArgumentOutOfRangeException.ThrowIfLessThan(currentIndex, -1); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(currentIndex, bars.Count); + + _bars = bars; + CurrentIndex = currentIndex; + } + + /// + public int CurrentIndex { get; private set; } + + /// + public int Count => CurrentIndex + 1; + + /// + public Bar Current + { + get + { + if (CurrentIndex < 0) + { + throw new InvalidOperationException("The bar window is not positioned on a bar yet."); + } + + return _bars[CurrentIndex]; + } + } + + /// + public Bar this[int index] + { + get + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (index > CurrentIndex) + { + throw LookAheadException.ForIndex("bars", index, CurrentIndex); + } + + return _bars[index]; + } + } + + /// + public Bar Ago(int offset) + { + ArgumentOutOfRangeException.ThrowIfNegative(offset); + + int index = CurrentIndex - offset; + if (index < 0) + { + throw new ArgumentOutOfRangeException( + nameof(offset), + offset, + string.Format( + CultureInfo.InvariantCulture, + "Stepping back {0} bars from bar {1} lands before the start of the series.", + offset, + CurrentIndex)); + } + + return _bars[index]; + } + + /// + /// Moves the cursor to a new bar. Only the engine may call this, and only forward, one bar at a time. + /// + /// The bar index to move to. + internal void MoveTo(int index) + { + CurrentIndex = index; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BollingerBandSeries.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BollingerBandSeries.cs new file mode 100644 index 00000000..aaf59c5b --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/BollingerBandSeries.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The three aligned outputs of the Bollinger Bands indicator. +/// +/// The upper band: the moving average plus deviationsUp standard deviations. +/// The middle band: the moving average itself. +/// The lower band: the moving average minus deviationsDown standard deviations. +public sealed record BollingerBandSeries(IndicatorSeries Upper, IndicatorSeries Middle, IndicatorSeries Lower); diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/EquityPoint.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/EquityPoint.cs new file mode 100644 index 00000000..52e508a1 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/EquityPoint.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// One sample of the equity curve, taken at the close of every bar, after any order scheduled for that bar +/// has been filled. +/// +/// The index of the bar in the source series. +/// The timestamp of the bar. +/// The close price of the bar, used for the mark-to-market valuation. +/// The cash balance of the account after all fills of the bar. +/// The signed exposure held at the close of the bar: positive long, negative short, zero flat. +/// The account equity, defined as Cash + SignedQuantity * Price. +public sealed record EquityPoint( + int BarIndex, + DateTime Timestamp, + double Price, + double Cash, + double SignedQuantity, + double Equity) +{ + /// + /// Gets a value indicating whether a position was open at the close of the bar. Used to compute exposure. + /// + public bool IsInPosition => SignedQuantity != 0.0; +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IBarWindow.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IBarWindow.cs new file mode 100644 index 00000000..ed362586 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IBarWindow.cs @@ -0,0 +1,78 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// A read-only, strictly causal view over the price series: everything up to and including the current bar, +/// and nothing beyond it. +/// +/// +/// +/// This is the only channel through which a strategy sees prices, and it is the structural half of the +/// no-look-ahead guarantee. Three properties make peeking impossible rather than merely discouraged: +/// +/// +/// +/// +/// reports the number of visible bars, so the total length of the series never +/// leaks. is window-relative for the same reason. +/// +/// +/// +/// +/// Any index strictly greater than throws , even +/// when a bar exists there β€” and it is thrown in preference to an out-of-range error, so the two answers +/// cannot be told apart and used to locate the end of the series. +/// +/// +/// +/// +/// No member returns the backing collection, so the future cannot be reached indirectly. +/// +/// +/// +/// +public interface IBarWindow +{ + /// + /// Gets the index of the bar currently being evaluated. This is the newest readable index. + /// + int CurrentIndex { get; } + + /// + /// Gets the number of visible bars, equal to CurrentIndex + 1. Bars beyond this point exist in + /// the source series but are deliberately not observable. + /// + int Count { get; } + + /// + /// Gets the bar currently being evaluated, that is the bar at . + /// + /// The window is not positioned on a bar yet. + Bar Current { get; } + + /// + /// Gets the bar at an absolute index in the source series. + /// + /// The absolute bar index. Must be in [0, CurrentIndex]. + /// The requested bar. + /// is negative. + /// is greater than . + Bar this[int index] { get; } + + /// + /// Gets a bar counted backwards from the current one. + /// + /// + /// The number of bars to step back. 0 is the current bar, 1 the previous one. + /// + /// The requested bar. + /// + /// is negative or steps before the start of the series. + /// + Bar Ago(int offset); +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IIndicatorSource.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IIndicatorSource.cs new file mode 100644 index 00000000..7be27ae8 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IIndicatorSource.cs @@ -0,0 +1,72 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The factory a strategy uses to declare the indicators it needs, once, before the backtest starts. +/// +/// +/// +/// Indicators are computed a single time over the whole series β€” which is both far faster than a +/// rolling recomputation and numerically identical to what TA-Lib would produce on a live feed, because every +/// function in the library is causal. The results are then handed back as +/// instances that are bound to the engine's bar cursor, so reading them ahead of the current bar throws. +/// +/// +/// Note what this interface deliberately does not offer: any way to obtain the raw price arrays. +/// A strategy is given indicator series and an , and neither of them can be +/// persuaded to reveal a future bar. That is what makes the no-look-ahead guarantee structural. +/// +/// +public interface IIndicatorSource +{ + /// + /// Creates a Simple Moving Average of the close prices. + /// + /// The averaging period in bars. + /// The aligned SMA series. + IndicatorSeries Sma(int timePeriod); + + /// + /// Creates an Exponential Moving Average of the close prices. + /// + /// The smoothing period in bars. + /// The aligned EMA series. + IndicatorSeries Ema(int timePeriod); + + /// + /// Creates a Relative Strength Index of the close prices. + /// + /// The RSI period in bars. + /// The aligned RSI series, valued in [0, 100]. + IndicatorSeries Rsi(int timePeriod); + + /// + /// Creates an Average True Range from the high, low and close prices. + /// + /// The ATR period in bars. + /// The aligned ATR series, expressed in price units. + IndicatorSeries Atr(int timePeriod); + + /// + /// Creates a Moving Average Convergence Divergence of the close prices. + /// + /// The fast EMA period in bars. + /// The slow EMA period in bars. + /// The signal EMA period in bars. + /// The aligned MACD line, signal line and histogram. + MacdSeries Macd(int fastPeriod, int slowPeriod, int signalPeriod); + + /// + /// Creates Bollinger Bands around a simple moving average of the close prices. + /// + /// The moving-average period in bars. + /// The number of standard deviations for the upper band. + /// The number of standard deviations for the lower band. + /// The aligned upper, middle and lower bands. + BollingerBandSeries BollingerBands(int timePeriod, double deviationsUp, double deviationsDown); +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IndicatorSeries.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IndicatorSeries.cs new file mode 100644 index 00000000..7eadfbb6 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IndicatorSeries.cs @@ -0,0 +1,307 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// A TA-Lib indicator output re-indexed onto the bar timeline, and β€” when bound to a bar window β€” subject to +/// the same no-look-ahead rule as the prices themselves. +/// +/// +/// +/// The alignment rule. Every TAMath entry point allocates its output as +/// new double[endIdx - startIdx + 1] and fills it from element 0, then reports where that +/// element belongs through . The mapping is therefore +/// +/// +/// output[k] describes bar (BegIdx + k) for k in [0, NBElement) +/// +/// +/// Elements at k >= NBElement are uninitialised zeros and carry no meaning. Reading the output array +/// as if element k described bar k shifts every signal backwards in time by BegIdx bars, +/// which is the single most common β€” and most silent β€” bug when consuming the library. This class performs +/// that mapping exactly once, in one place, and every strategy in this sample goes through it. +/// +/// +/// The causality rule. When constructed with a bar window, reading a bar index greater than +/// throws β€” an indicator value computed +/// from tomorrow's close is exactly as fatal as reading tomorrow's close directly. Passing +/// for the window disables the guard, which is intended for unit-testing the alignment +/// arithmetic in isolation; the engine always binds a window. +/// +/// +/// The metadata rule. Causality has to cover the metadata too, or it is only a convention. A bound +/// series reports , and as of the current +/// bar: Count is CurrentIndex + 1, and NBElement counts only the values that have +/// already happened. Otherwise a strategy could read the total number of bars out of Count in +/// Initialize and size up near a known end date β€” a look-ahead that never touches a price. The full +/// length is kept privately for bounds checking and is never observable. +/// +/// +public sealed class IndicatorSeries +{ + private readonly double[] _values; + private readonly bool[] _hasValue; + private readonly IBarWindow? _window; + private readonly int _barCount; + private readonly int _begIdx; + private readonly int _nbElement; + + /// + /// Initializes a new instance of the class by mapping a raw TA-Lib output + /// array onto the bar timeline. + /// + /// A short name used in diagnostics, for example "SMA(50)". + /// The number of bars in the source series. + /// The BegIdx reported by TA-Lib: the bar index that output element 0 describes. + /// The NBElement reported by TA-Lib: the number of meaningful output elements. + /// The raw output array. Only its first elements are read. + /// + /// The bar window that enforces causality, or to disable the guard. + /// + /// + /// or is . + /// + /// + /// , or is negative. + /// + public IndicatorSeries(string name, int barCount, int begIdx, int nbElement, IReadOnlyList output, IBarWindow? window) + { + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(output); + ArgumentOutOfRangeException.ThrowIfNegative(barCount); + ArgumentOutOfRangeException.ThrowIfNegative(begIdx); + ArgumentOutOfRangeException.ThrowIfNegative(nbElement); + + Name = name; + _barCount = barCount; + _begIdx = begIdx; + _nbElement = nbElement; + _window = window; + _values = new double[barCount]; + _hasValue = new bool[barCount]; + + // The one and only place where TA-Lib's (BegIdx, NBElement) convention is translated into bar indices. + int usable = Math.Min(nbElement, output.Count); + for (int k = 0; k < usable; k++) + { + int barIndex = begIdx + k; + if (barIndex >= barCount) + { + break; + } + + _values[barIndex] = output[k]; + _hasValue[barIndex] = true; + } + } + + /// + /// Gets the diagnostic name of the series, for example "RSI(14)". + /// + public string Name { get; } + + /// + /// Gets the number of visible bars: CurrentIndex + 1 while bound to a bar window, and the + /// full bar count only for an unbound series. The total length of the series never leaks through it. + /// + public int Count => _window is null ? _barCount : _window.CurrentIndex + 1; + + /// + /// Gets the bar index of the first bar carrying a value, as reported by TA-Lib. Bars before it are + /// consumed by the indicator's lookback and hold no value. Reads as -1 while bound to a window + /// that has not yet reached the first valid bar, because until then even that index is a fact about the + /// future. + /// + public int BegIdx + { + get + { + if (_nbElement == 0) + { + return -1; + } + + return _window is null || _begIdx <= _window.CurrentIndex ? _begIdx : -1; + } + } + + /// + /// Gets the number of meaningful values TA-Lib produced, clamped to the visible region while bound to a + /// bar window. BegIdx + NBElement is therefore never a probe for the end of the series. + /// + public int NBElement + { + get + { + if (_window is null) + { + return _nbElement; + } + + int visible = _window.CurrentIndex - _begIdx + 1; + return Math.Clamp(visible, 0, _nbElement); + } + } + + /// + /// Gets the value of the indicator on the bar the backtest is currently positioned on. + /// + /// + /// The series is not bound to a bar window, or the window has no current bar, or the current bar carries + /// no value. + /// + public double Current + { + get + { + if (_window is null) + { + throw new InvalidOperationException( + string.Format(CultureInfo.InvariantCulture, "Indicator '{0}' is not bound to a bar window.", Name)); + } + + return this[_window.CurrentIndex]; + } + } + + /// + /// Gets a value indicating whether the bar the backtest is currently positioned on carries a value. + /// Returns while the indicator is still warming up. + /// + public bool HasCurrent => _window is not null && _window.CurrentIndex >= 0 && HasValueAt(_window.CurrentIndex); + + /// + /// Gets the indicator value that belongs to a specific bar. + /// + /// The absolute bar index. + /// The indicator value for that bar. + /// + /// is outside the bar series. + /// + /// + /// The series is bound to a window and is in the future. + /// + /// + /// The bar carries no value because the indicator is still warming up. Use to + /// test first. + /// + public double this[int barIndex] + { + get + { + Guard(barIndex); + + if (!_hasValue[barIndex]) + { + throw new InvalidOperationException( + string.Format( + CultureInfo.InvariantCulture, + "Indicator '{0}' has no value on bar {1}; the first valid bar is {2}.", + Name, + barIndex, + _begIdx)); + } + + return _values[barIndex]; + } + } + + /// + /// Determines whether a specific bar carries a value, taking the look-ahead guard into account. + /// + /// The absolute bar index. + /// when the bar carries a value; otherwise . + /// + /// is outside the bar series. + /// + /// + /// The series is bound to a window and is in the future. + /// + public bool HasValueAt(int barIndex) + { + Guard(barIndex); + return _hasValue[barIndex]; + } + + /// + /// Attempts to read the indicator value of a specific bar. This is the preferred accessor for strategy + /// code, because it reports the warm-up period without throwing. + /// + /// The absolute bar index. + /// + /// When this method returns , the indicator value; otherwise 0. + /// + /// when the bar carries a value; otherwise . + /// + /// is outside the bar series. + /// + /// + /// The series is bound to a window and is in the future. + /// + public bool TryGetValue(int barIndex, out double value) + { + Guard(barIndex); + + value = _hasValue[barIndex] ? _values[barIndex] : 0.0; + return _hasValue[barIndex]; + } + + /// + /// Attempts to read the indicator value of the current bar and of the bar before it, which is what a + /// crossover test needs. + /// + /// The absolute bar index of the newer of the two bars. + /// + /// When this method returns , the value on bar barIndex - 1. + /// + /// + /// When this method returns , the value on bar barIndex. + /// + /// when both bars carry a value; otherwise . + /// + /// The series is bound to a window and is in the future. + /// + public bool TryGetPair(int barIndex, out double previous, out double current) + { + previous = 0.0; + current = 0.0; + + // Causality is tested before the range test on purpose. If a past-the-end index returned false while + // a future index threw, the difference between the two answers would be an oracle for the total + // length of the series, findable by binary search without ever reading a value. + if (barIndex > 0 && _window is not null && barIndex > _window.CurrentIndex) + { + throw LookAheadException.ForIndex(Name, barIndex, _window.CurrentIndex); + } + + if (barIndex < 1 || barIndex >= _barCount) + { + return false; + } + + bool hasPrevious = TryGetValue(barIndex - 1, out previous); + bool hasCurrent = TryGetValue(barIndex, out current); + + return hasPrevious && hasCurrent; + } + + private void Guard(int barIndex) + { + ArgumentOutOfRangeException.ThrowIfNegative(barIndex); + + // Causality is checked before the upper-range test, and the range test uses the private full length + // rather than the visible Count. A bound series therefore answers *every* forward index with + // LookAheadException β€” if it answered "out of range" past the end and "look-ahead" before it, the + // pair of answers would spell out the total length of the series. + if (_window is not null && barIndex > _window.CurrentIndex) + { + throw LookAheadException.ForIndex(Name, barIndex, _window.CurrentIndex); + } + + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(barIndex, _barCount); + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IndicatorSet.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IndicatorSet.cs new file mode 100644 index 00000000..66dd9cb4 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/IndicatorSet.cs @@ -0,0 +1,247 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The default . It extracts the OHLC arrays once, calls the TA-Lib wrappers in +/// TAMath over the whole series, and re-indexes every output through . +/// +/// +/// Requests are memoised by name and parameters, so a strategy asking twice for SMA(50) pays for one +/// computation. The price arrays are private and are never handed out β€” see +/// for why that matters. +/// +public sealed class IndicatorSet : IIndicatorSource +{ + private readonly Dictionary _cache = new(StringComparer.Ordinal); + private readonly double[] _open; + private readonly double[] _high; + private readonly double[] _low; + private readonly double[] _close; + private readonly IBarWindow? _window; + + /// + /// Initializes a new instance of the class. + /// + /// The full bar series the indicators are computed over. + /// + /// The bar window every produced series is bound to, or to produce unguarded + /// series (unit tests only). + /// + /// is . + public IndicatorSet(IReadOnlyList bars, IBarWindow? window) + { + ArgumentNullException.ThrowIfNull(bars); + + _window = window; + BarCount = bars.Count; + _open = new double[BarCount]; + _high = new double[BarCount]; + _low = new double[BarCount]; + _close = new double[BarCount]; + + for (int i = 0; i < BarCount; i++) + { + Bar bar = bars[i]; + _open[i] = bar.Open; + _high[i] = bar.High; + _low[i] = bar.Low; + _close[i] = bar.Close; + } + } + + /// + /// Gets the number of bars the indicators span. + /// + public int BarCount { get; } + + /// + public IndicatorSeries Sma(int timePeriod) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 1); + + return GetOrAdd( + Key("SMA", timePeriod), + key => + { + if (BarCount == 0) + { + return Empty(key); + } + + SmaResult result = TAMath.Sma(0, BarCount - 1, _close, timePeriod); + return Wrap(key, result, result.Real); + }); + } + + /// + public IndicatorSeries Ema(int timePeriod) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 1); + + return GetOrAdd( + Key("EMA", timePeriod), + key => + { + if (BarCount == 0) + { + return Empty(key); + } + + EmaResult result = TAMath.Ema(0, BarCount - 1, _close, timePeriod); + return Wrap(key, result, result.Real); + }); + } + + /// + public IndicatorSeries Rsi(int timePeriod) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 1); + + return GetOrAdd( + Key("RSI", timePeriod), + key => + { + if (BarCount == 0) + { + return Empty(key); + } + + RsiResult result = TAMath.Rsi(0, BarCount - 1, _close, timePeriod); + return Wrap(key, result, result.Real); + }); + } + + /// + public IndicatorSeries Atr(int timePeriod) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 1); + + return GetOrAdd( + Key("ATR", timePeriod), + key => + { + if (BarCount == 0) + { + return Empty(key); + } + + AtrResult result = TAMath.Atr(0, BarCount - 1, _high, _low, _close, timePeriod); + return Wrap(key, result, result.Real); + }); + } + + /// + public MacdSeries Macd(int fastPeriod, int slowPeriod, int signalPeriod) + { + ArgumentOutOfRangeException.ThrowIfLessThan(fastPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(slowPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(signalPeriod, 1); + + string key = Key("MACD", fastPeriod, slowPeriod, signalPeriod); + if (_cache.TryGetValue(key, out object? cached)) + { + return (MacdSeries)cached; + } + + MacdSeries series; + if (BarCount == 0) + { + series = new MacdSeries(Empty(key + ".line"), Empty(key + ".signal"), Empty(key + ".hist")); + } + else + { + MacdResult result = TAMath.Macd(0, BarCount - 1, _close, fastPeriod, slowPeriod, signalPeriod); + series = new MacdSeries( + Wrap(key + ".line", result, result.MacdValue), + Wrap(key + ".signal", result, result.MacdSignal), + Wrap(key + ".hist", result, result.MacdHist)); + } + + _cache[key] = series; + return series; + } + + /// + public BollingerBandSeries BollingerBands(int timePeriod, double deviationsUp, double deviationsDown) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 1); + + string key = string.Format( + CultureInfo.InvariantCulture, + "BBANDS({0},{1},{2})", + timePeriod, + deviationsUp, + deviationsDown); + + if (_cache.TryGetValue(key, out object? cached)) + { + return (BollingerBandSeries)cached; + } + + BollingerBandSeries series; + if (BarCount == 0) + { + series = new BollingerBandSeries(Empty(key + ".upper"), Empty(key + ".middle"), Empty(key + ".lower")); + } + else + { + BollingerBandsResult result = TAMath.BollingerBands( + 0, + BarCount - 1, + _close, + timePeriod, + deviationsUp, + deviationsDown, + MAType.Sma); + + series = new BollingerBandSeries( + Wrap(key + ".upper", result, result.RealUpperBand), + Wrap(key + ".middle", result, result.RealMiddleBand), + Wrap(key + ".lower", result, result.RealLowerBand)); + } + + _cache[key] = series; + return series; + } + + private static string Key(string name, params int[] parameters) + { + return string.Format( + CultureInfo.InvariantCulture, + "{0}({1})", + name, + string.Join(',', parameters)); + } + + private IndicatorSeries Wrap(string name, IndicatorResult result, double[] output) + { + // A non-success return code means TA-Lib produced nothing usable. Modelling that as a series with + // zero valid elements lets a strategy simply stay flat instead of trading on garbage. + int nbElement = result.RetCode == RetCode.Success ? result.NBElement : 0; + int begIdx = result.RetCode == RetCode.Success ? result.BegIdx : 0; + + return new IndicatorSeries(name, BarCount, begIdx, nbElement, output, _window); + } + + private IndicatorSeries Empty(string name) + { + return new IndicatorSeries(name, BarCount, 0, 0, [], _window); + } + + private IndicatorSeries GetOrAdd(string key, Func factory) + { + if (_cache.TryGetValue(key, out object? cached)) + { + return (IndicatorSeries)cached; + } + + IndicatorSeries series = factory(key); + _cache[key] = series; + return series; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/LookAheadException.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/LookAheadException.cs new file mode 100644 index 00000000..516a383f --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/LookAheadException.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// Thrown when strategy code attempts to read data that would not have been available at the moment the +/// decision is being taken β€” the defining bug of a broken backtest. +/// +/// +/// This exception is the enforcement mechanism behind the no-look-ahead guarantee of +/// . It is raised by and by +/// when an index strictly greater than the current bar index is requested. +/// It is deliberately not caught by the engine: a strategy that peeks into the future must fail +/// loudly rather than silently produce impossible returns. +/// +public sealed class LookAheadException : InvalidOperationException +{ + /// + /// Initializes a new instance of the class. + /// + public LookAheadException() + : base("Look-ahead bias detected: strategy code attempted to read data from the future.") + { + } + + /// + /// Initializes a new instance of the class with a specified message. + /// + /// The message that describes the error. + public LookAheadException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified message + /// and a reference to the inner exception that caused it. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + public LookAheadException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Creates an exception describing an attempt to read bar while the + /// backtest is positioned on bar . + /// + /// A short description of the series being read, for example "bars" or an indicator name. + /// The bar index the caller tried to read. + /// The index of the most recent bar the caller is allowed to read. + /// A carrying a diagnostic message. + public static LookAheadException ForIndex(string source, int requestedIndex, int currentIndex) + { + string message = string.Format( + CultureInfo.InvariantCulture, + "Look-ahead bias detected while reading '{0}': index {1} lies in the future; the backtest is positioned on bar {2}. " + + "A decision taken on bar {2} may only use data from bars 0..{2}.", + source, + requestedIndex, + currentIndex); + + return new LookAheadException(message); + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/MacdSeries.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/MacdSeries.cs new file mode 100644 index 00000000..1f01ba3e --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/MacdSeries.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The three aligned outputs of the MACD indicator. +/// +/// The MACD line: fast EMA minus slow EMA. +/// The signal line: an EMA of . +/// The histogram: minus . +public sealed record MacdSeries(IndicatorSeries Line, IndicatorSeries Signal, IndicatorSeries Histogram); diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/OrderSide.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/OrderSide.cs new file mode 100644 index 00000000..c064380f --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/OrderSide.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// Identifies the direction of an order. It is also used to describe the direction of a position or of a +/// completed round trip, in which case it refers to the side of the order that opened the position. +/// +public enum OrderSide +{ + /// + /// A buy order. When it opens a position, the resulting position is long (positive quantity). + /// + Buy = 0, + + /// + /// A sell order. When it opens a position, the resulting position is short (negative quantity). + /// + Sell = 1 +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Position.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Position.cs new file mode 100644 index 00000000..48b60e53 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Position.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// An open position. Instances are immutable snapshots handed to a strategy; the engine replaces the +/// snapshot whenever the position changes. +/// +/// +/// The side of the order that opened the position. is long, +/// is short. +/// +/// +/// The absolute number of units held. Always strictly positive; use for the +/// signed exposure. +/// +/// The realised fill price of the opening order, slippage already applied. +/// The index of the bar whose open filled the position. +/// The timestamp of the bar whose open filled the position. +/// The commission charged on the opening order, in account currency. +public sealed record Position( + OrderSide Side, + double Quantity, + double EntryPrice, + int EntryIndex, + DateTime EntryTime, + double EntryCommission) +{ + /// + /// Gets a value indicating whether the position is long. + /// + public bool IsLong => Side == OrderSide.Buy; + + /// + /// Gets a value indicating whether the position is short. + /// + public bool IsShort => Side == OrderSide.Sell; + + /// + /// Gets the signed exposure: +Quantity when long, -Quantity when short. + /// + public double SignedQuantity => IsLong ? Quantity : -Quantity; + + /// + /// Gets the mark-to-market value of the position at the supplied price, defined as + /// SignedQuantity * price. + /// + /// The price used to value the position. + /// The signed market value, negative for a short position. + public double MarketValue(double price) + { + return SignedQuantity * price; + } + + /// + /// Gets the unrealised profit and loss of the position at the supplied price, before exit costs, + /// defined as SignedQuantity * (price - EntryPrice). + /// + /// The price used to value the position. + /// The unrealised profit (positive) or loss (negative) in account currency. + public double UnrealizedProfit(double price) + { + return SignedQuantity * (price - EntryPrice); + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/PositionSizing.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/PositionSizing.cs new file mode 100644 index 00000000..38bebc2f --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/PositionSizing.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The policy used to convert a signal into a traded quantity. +/// +public enum PositionSizing +{ + /// + /// Allocate a fixed fraction of the current account equity to every new position. + /// The fraction is . + /// + FixedFraction = 0, + + /// + /// Allocate a fixed cash notional to every new position, capped by the current account equity. + /// The notional is . + /// + FixedCash = 1 +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Signal.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Signal.cs new file mode 100644 index 00000000..1dd4a55f --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Signal.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// The desired target exposure emitted by a strategy after it has observed a bar. +/// +/// +/// A signal expresses a target state, not a delta. Emitting while already long +/// is a no-op; emitting it while short reverses the position in a single fill at the next open. Because a signal +/// is always produced from the close of bar i, the engine can only act on it at the open of bar +/// i + 1. See for the full execution timeline. +/// +public enum Signal +{ + /// + /// Keep the current exposure unchanged. No order is sent. + /// + Hold = 0, + + /// + /// Target a long position. Any open short is covered first, at the same fill. + /// + EnterLong = 1, + + /// + /// Target a short position. Any open long is closed first, at the same fill. + /// When is the engine downgrades this + /// signal to , so the strategy simply goes flat instead of short. + /// + EnterShort = 2, + + /// + /// Target a flat position. Any open position is closed. No-op when already flat. + /// + Exit = 3 +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Trade.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Trade.cs new file mode 100644 index 00000000..ec30c5f7 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Engine/Trade.cs @@ -0,0 +1,87 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Engine; + +/// +/// A completed round trip: one opening fill and one closing fill of the same quantity. +/// +/// +/// Both prices are realised fill prices, so slippage is already baked into them. Commission is not; +/// carries the sum of the entry and the exit commission and is subtracted by +/// . +/// +/// +/// The side of the opening order. is a long round trip, +/// a short one. +/// +/// The absolute number of units traded. +/// The index of the bar whose open filled the entry. +/// The timestamp of the bar whose open filled the entry. +/// The realised entry fill price, slippage already applied. +/// The index of the bar that filled the exit. +/// The timestamp of the bar that filled the exit. +/// The realised exit fill price, slippage already applied. +/// The total commission of the round trip: entry commission plus exit commission. +public sealed record Trade( + OrderSide Side, + double Quantity, + int EntryIndex, + DateTime EntryTime, + double EntryPrice, + int ExitIndex, + DateTime ExitTime, + double ExitPrice, + double Commission) +{ + /// + /// Gets a value indicating whether the round trip was long. + /// + public bool IsLong => Side == OrderSide.Buy; + + /// + /// Gets the profit and loss before commission, defined as + /// (ExitPrice - EntryPrice) * Quantity for a long and + /// (EntryPrice - ExitPrice) * Quantity for a short. + /// + public double GrossProfit => (IsLong ? ExitPrice - EntryPrice : EntryPrice - ExitPrice) * Quantity; + + /// + /// Gets the profit and loss after commission, defined as GrossProfit - Commission. + /// This is the value used by every trade-based performance metric. + /// + public double NetProfit => GrossProfit - Commission; + + /// + /// Gets the net return on the entry notional, defined as NetProfit / (EntryPrice * Quantity). + /// Returns 0 when the entry notional is zero. + /// + public double ReturnOnNotional + { + get + { + double notional = EntryPrice * Quantity; + return notional > 0.0 ? NetProfit / notional : 0.0; + } + } + + /// + /// Gets the holding period in bars, defined as ExitIndex - EntryIndex. + /// + public int BarsHeld => ExitIndex - EntryIndex; + + /// + /// Gets a value indicating whether the round trip was profitable after commission + /// (strictly positive ). + /// + public bool IsWin => NetProfit > 0.0; + + /// + /// Gets a value indicating whether the round trip lost money after commission + /// (strictly negative ). + /// + public bool IsLoss => NetProfit < 0.0; +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/GlobalUsings.cs b/samples/TechnicalAnalysis.Samples.Backtesting/GlobalUsings.cs new file mode 100644 index 00000000..1ade2c13 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/GlobalUsings.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +global using System.Globalization; +global using TechnicalAnalysis.Common; +global using TechnicalAnalysis.Functions; +global using TechnicalAnalysis.Samples.Backtesting.Data; +global using TechnicalAnalysis.Samples.Backtesting.Engine; +global using TechnicalAnalysis.Samples.Backtesting.Metrics; +global using TechnicalAnalysis.Samples.Backtesting.Reporting; +global using TechnicalAnalysis.Samples.Backtesting.Strategies; + +// The library's fluent API introduces TechnicalAnalysis.Functions.IndicatorSeries and MacdSeries, +// which collide by name with this sample's engine types of the same name. The sample keeps its own: +// they are bar-window aware and clamp their metadata so a strategy cannot read a future bar. Pin +// them explicitly rather than relying on using order. +global using IndicatorSeries = TechnicalAnalysis.Samples.Backtesting.Engine.IndicatorSeries; +global using MacdSeries = TechnicalAnalysis.Samples.Backtesting.Engine.MacdSeries; diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Metrics/PerformanceMetrics.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Metrics/PerformanceMetrics.cs new file mode 100644 index 00000000..d6a67290 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Metrics/PerformanceMetrics.cs @@ -0,0 +1,484 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Metrics; + +/// +/// The performance statistics of one backtest run. +/// +/// +/// +/// Notation. E(0..n-1) is the equity curve, one point per bar, sampled at the close of each bar +/// after that bar's fills. The per-bar simple returns are +/// r(t) = E(t) / E(t-1) - 1 for t = 1..n-1, so there are n - 1 of them. +/// +/// +/// Annualisation. Every annualised figure uses the constant , which must +/// match the bar interval of the data (252 for daily equity bars, 365 for daily crypto bars, 52 weekly, +/// 12 monthly). Rates are scaled by BarsPerYear and standard deviations by +/// sqrt(BarsPerYear) β€” the usual i.i.d. square-root-of-time assumption, which understates risk when +/// returns are autocorrelated. The horizon in years is (n - 1) / BarsPerYear: it counts bar +/// intervals, not equity points. +/// +/// +/// Degenerate inputs. No member ever returns . An empty or single-point +/// equity curve, a curve with zero volatility, a run with no trades and a run with no winning trades all +/// produce defined values; the individual members document which. The only non-finite value that can be +/// produced is from , which is the +/// conventional reading of "wins and no losses". +/// +/// +public sealed record PerformanceMetrics +{ + private PerformanceMetrics() + { + } + + /// + /// Gets the metrics of a run that never happened: every figure zero. Used for an empty bar series. + /// + public static PerformanceMetrics Empty { get; } = new(); + + /// + /// Gets the number of equity points, which equals the number of bars simulated. + /// + public int BarCount { get; private init; } + + /// + /// Gets the annualisation constant used by every annualised figure in this record: the number of bars + /// that make up one year. + /// + public int BarsPerYear { get; private init; } + + /// + /// Gets the starting equity of the account. + /// + public double InitialCapital { get; private init; } + + /// + /// Gets the equity at the close of the last bar. + /// + public double FinalEquity { get; private init; } + + /// + /// Gets the total return over the whole run: FinalEquity / InitialCapital - 1. + /// A value of 0.25 means the account grew by 25%. Zero when the curve is empty. + /// + public double TotalReturn { get; private init; } + + /// + /// Gets the compound annual growth rate: + /// (FinalEquity / InitialCapital) ^ (1 / years) - 1 with years = (BarCount - 1) / BarsPerYear. + /// Zero when fewer than two bars were simulated (no elapsed time to compound over); + /// -1 (total loss) when the account was wiped out. + /// + public double Cagr { get; private init; } + + /// + /// Gets the annualised volatility of the per-bar returns: + /// stdev(r) * sqrt(BarsPerYear), where stdev is the sample standard deviation + /// (Bessel-corrected, divisor n - 2 for n - 1 returns). Zero when fewer than two returns + /// exist, and zero for a perfectly flat curve. + /// + public double AnnualizedVolatility { get; private init; } + + /// + /// Gets the maximum peak-to-trough decline of the equity curve, as a positive fraction: + /// max over t of (peak(t) - E(t)) / peak(t) where peak(t) = max(E(0..t)). + /// A value of 0.30 means the account was once 30% below its running high. Zero for a curve that + /// never declines. + /// + public double MaxDrawdown { get; private init; } + + /// + /// Gets the length in bars of the longest drawdown: the greatest number of bars between the bar that set + /// a running peak and the first later bar whose equity reaches that peak again. A drawdown still open on + /// the last bar is measured up to that last bar. Zero when the curve never declines. + /// + public int MaxDrawdownDurationBars { get; private init; } + + /// + /// Gets the annualised Sharpe ratio: + /// mean(r - rf) / stdev(r) * sqrt(BarsPerYear), where rf is the per-bar risk-free rate + /// obtained by de-annualising geometrically + /// ((1 + rate) ^ (1 / BarsPerYear) - 1) and stdev is the sample standard deviation of the + /// raw returns. Zero when volatility is zero or fewer than two returns exist. + /// + public double Sharpe { get; private init; } + + /// + /// Gets the annualised Sortino ratio: + /// mean(r - rf) / downside(r) * sqrt(BarsPerYear), where the downside deviation is the + /// root-mean-square of the shortfalls against the risk-free rate, averaged over all returns + /// rather than only the negative ones: + /// downside(r) = sqrt( sum over t of min(r(t) - rf, 0)^2 / count(r) ). + /// Zero when no return falls below the risk-free rate (no downside to divide by). + /// + public double Sortino { get; private init; } + + /// + /// Gets the Calmar ratio: Cagr / MaxDrawdown, using the same annualisation as + /// . Zero when the maximum drawdown is zero. + /// + public double Calmar { get; private init; } + + /// + /// Gets the annual risk-free rate that and were measured + /// against, as a decimal fraction. + /// + public double RiskFreeRate { get; private init; } + + /// + /// Gets the number of completed round trips. A position still open on the last bar is liquidated by the + /// engine, so it is counted here too. + /// + public int TradeCount { get; private init; } + + /// + /// Gets the number of round trips with a strictly positive net profit. + /// + public int WinCount { get; private init; } + + /// + /// Gets the number of round trips with a strictly negative net profit. Break-even trades count as + /// neither a win nor a loss. + /// + public int LossCount { get; private init; } + + /// + /// Gets the fraction of round trips that made money: WinCount / TradeCount. + /// Zero when there were no trades. + /// + public double WinRate { get; private init; } + + /// + /// Gets the ratio of gross profit to gross loss: + /// sum of positive net profits / |sum of negative net profits|, both after commission. + /// Zero when there were no trades; when there were winning trades + /// and no losing ones. + /// + public double ProfitFactor { get; private init; } + + /// + /// Gets the mean net profit of the winning round trips, in account currency. Zero when there were none. + /// + public double AverageWin { get; private init; } + + /// + /// Gets the mean net profit of the losing round trips, in account currency. Reported as a + /// negative number. Zero when there were none. + /// + public double AverageLoss { get; private init; } + + /// + /// Gets the expected net profit per round trip, in account currency: + /// sum of net profits / TradeCount. This is identical to the textbook + /// WinRate * AverageWin + (1 - WinRate) * AverageLoss whenever no trade is exactly break-even. + /// Zero when there were no trades. + /// + public double Expectancy { get; private init; } + + /// + /// Gets the fraction of bars on which a position was open at the close: + /// count(bars with a non-zero position) / BarCount. A value of 0.40 means the account was + /// exposed to the market 40% of the time. Zero when the curve is empty. + /// + public double Exposure { get; private init; } + + /// + /// Gets the highest equity ever reached during the run. Equals the initial capital when the curve is + /// empty. + /// + public double PeakEquity { get; private init; } + + /// + /// Computes the metrics of a run. + /// + /// The equity curve, one point per bar. May be empty. + /// The completed round trips. May be empty. + /// The starting equity. Must be strictly positive. + /// + /// The annualisation constant: the number of bars in one year. Must be strictly positive. + /// + /// + /// The annual risk-free rate as a decimal fraction, used by Sharpe and Sortino. Defaults to 0. + /// + /// The computed metrics, or when the equity curve holds no points. + /// + /// or is . + /// + /// + /// or is not strictly positive. + /// + public static PerformanceMetrics Compute( + IReadOnlyList equityCurve, + IReadOnlyList trades, + double initialCapital, + int barsPerYear, + double riskFreeRate = 0.0) + { + ArgumentNullException.ThrowIfNull(equityCurve); + ArgumentNullException.ThrowIfNull(trades); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(initialCapital); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(barsPerYear); + + if (equityCurve.Count == 0) + { + return Empty with + { + BarsPerYear = barsPerYear, + InitialCapital = initialCapital, + FinalEquity = initialCapital, + PeakEquity = initialCapital, + RiskFreeRate = riskFreeRate + }; + } + + int barCount = equityCurve.Count; + double finalEquity = equityCurve[^1].Equity; + double totalReturn = (finalEquity / initialCapital) - 1.0; + + double[] returns = ComputeReturns(equityCurve); + DrawdownStatistics drawdown = ComputeDrawdown(equityCurve); + TradeStatistics tradeStats = ComputeTradeStatistics(trades); + + double riskFreePerBar = Math.Pow(1.0 + riskFreeRate, 1.0 / barsPerYear) - 1.0; + double meanReturn = Mean(returns); + double sampleStdDev = SampleStandardDeviation(returns, meanReturn); + double downsideDeviation = DownsideDeviation(returns, riskFreePerBar); + double annualizationFactor = Math.Sqrt(barsPerYear); + double meanExcess = returns.Length > 0 ? meanReturn - riskFreePerBar : 0.0; + + double years = (barCount - 1) / (double)barsPerYear; + double cagr = ComputeCagr(initialCapital, finalEquity, years); + + int exposedBars = 0; + foreach (EquityPoint point in equityCurve) + { + if (point.IsInPosition) + { + exposedBars++; + } + } + + return new PerformanceMetrics + { + BarCount = barCount, + BarsPerYear = barsPerYear, + InitialCapital = initialCapital, + FinalEquity = finalEquity, + TotalReturn = totalReturn, + Cagr = cagr, + AnnualizedVolatility = sampleStdDev * annualizationFactor, + MaxDrawdown = drawdown.MaxDrawdown, + MaxDrawdownDurationBars = drawdown.MaxDurationBars, + PeakEquity = drawdown.PeakEquity, + Sharpe = sampleStdDev > 0.0 ? meanExcess / sampleStdDev * annualizationFactor : 0.0, + Sortino = downsideDeviation > 0.0 ? meanExcess / downsideDeviation * annualizationFactor : 0.0, + Calmar = drawdown.MaxDrawdown > 0.0 ? cagr / drawdown.MaxDrawdown : 0.0, + RiskFreeRate = riskFreeRate, + TradeCount = tradeStats.Count, + WinCount = tradeStats.Wins, + LossCount = tradeStats.Losses, + WinRate = tradeStats.WinRate, + ProfitFactor = tradeStats.ProfitFactor, + AverageWin = tradeStats.AverageWin, + AverageLoss = tradeStats.AverageLoss, + Expectancy = tradeStats.Expectancy, + Exposure = (double)exposedBars / barCount + }; + } + + private static double ComputeCagr(double initialCapital, double finalEquity, double years) + { + if (years <= 0.0) + { + return 0.0; + } + + if (finalEquity <= 0.0) + { + return -1.0; + } + + return Math.Pow(finalEquity / initialCapital, 1.0 / years) - 1.0; + } + + private static double[] ComputeReturns(IReadOnlyList equityCurve) + { + if (equityCurve.Count < 2) + { + return []; + } + + double[] returns = new double[equityCurve.Count - 1]; + for (int t = 1; t < equityCurve.Count; t++) + { + double previous = equityCurve[t - 1].Equity; + + // A wiped-out account has no meaningful percentage return; treat the step as flat rather than + // letting a division by zero poison every downstream statistic with NaN or infinity. + returns[t - 1] = previous > 0.0 ? (equityCurve[t].Equity / previous) - 1.0 : 0.0; + } + + return returns; + } + + private static DrawdownStatistics ComputeDrawdown(IReadOnlyList equityCurve) + { + double peak = equityCurve[0].Equity; + int peakIndex = 0; + double maxDrawdown = 0.0; + int maxDuration = 0; + bool belowPeak = false; + + for (int i = 0; i < equityCurve.Count; i++) + { + double equity = equityCurve[i].Equity; + + if (equity >= peak) + { + // The recovery bar is part of the drawdown's length: the documented measure runs from the + // bar that set the peak to the first later bar that reaches it again. Measuring only while + // still under water would report every recovered drawdown one bar short. + if (belowPeak) + { + maxDuration = Math.Max(maxDuration, i - peakIndex); + belowPeak = false; + } + + peak = equity; + peakIndex = i; + continue; + } + + belowPeak = true; + + if (peak > 0.0) + { + maxDrawdown = Math.Max(maxDrawdown, (peak - equity) / peak); + } + + // A drawdown still open on the last bar is measured up to that last bar. + maxDuration = Math.Max(maxDuration, i - peakIndex); + } + + return new DrawdownStatistics(maxDrawdown, maxDuration, peak); + } + + private static TradeStatistics ComputeTradeStatistics(IReadOnlyList trades) + { + if (trades.Count == 0) + { + return new TradeStatistics(0, 0, 0, 0.0, 0.0, 0.0, 0.0, 0.0); + } + + int wins = 0; + int losses = 0; + double grossProfit = 0.0; + double grossLoss = 0.0; + double netTotal = 0.0; + + foreach (Trade trade in trades) + { + double net = trade.NetProfit; + netTotal += net; + + if (net > 0.0) + { + wins++; + grossProfit += net; + } + else if (net < 0.0) + { + losses++; + grossLoss += -net; + } + } + + double profitFactor; + if (grossLoss > 0.0) + { + profitFactor = grossProfit / grossLoss; + } + else + { + profitFactor = grossProfit > 0.0 ? double.PositiveInfinity : 0.0; + } + + return new TradeStatistics( + trades.Count, + wins, + losses, + (double)wins / trades.Count, + profitFactor, + wins > 0 ? grossProfit / wins : 0.0, + losses > 0 ? -grossLoss / losses : 0.0, + netTotal / trades.Count); + } + + private static double Mean(double[] values) + { + if (values.Length == 0) + { + return 0.0; + } + + double sum = 0.0; + foreach (double value in values) + { + sum += value; + } + + return sum / values.Length; + } + + private static double SampleStandardDeviation(double[] values, double mean) + { + if (values.Length < 2) + { + return 0.0; + } + + double sumSquares = 0.0; + foreach (double value in values) + { + double deviation = value - mean; + sumSquares += deviation * deviation; + } + + return Math.Sqrt(sumSquares / (values.Length - 1)); + } + + private static double DownsideDeviation(double[] values, double minimumAcceptableReturn) + { + if (values.Length == 0) + { + return 0.0; + } + + double sumSquares = 0.0; + foreach (double value in values) + { + double shortfall = Math.Min(value - minimumAcceptableReturn, 0.0); + sumSquares += shortfall * shortfall; + } + + return Math.Sqrt(sumSquares / values.Length); + } + + private readonly record struct DrawdownStatistics(double MaxDrawdown, int MaxDurationBars, double PeakEquity); + + private readonly record struct TradeStatistics( + int Count, + int Wins, + int Losses, + double WinRate, + double ProfitFactor, + double AverageWin, + double AverageLoss, + double Expectancy); +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Program.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Program.cs new file mode 100644 index 00000000..3ffaed07 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Program.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting; + +/// +/// Entry point of the backtesting sample. +/// +internal static class Program +{ + /// + /// Parses the command line, runs the sample and prints its report. + /// + /// The command-line arguments. + /// 0 on success, 1 when the command line or the input data was rejected. + internal static int Main(string[] args) + { + CommandLineOptions options; + try + { + options = CommandLineOptions.Parse(args); + } + catch (FormatException ex) + { + Console.Error.WriteLine(ex.Message); + Console.Error.WriteLine(); + Console.Error.WriteLine(CommandLineOptions.Usage); + return 1; + } + + if (options.ShowHelp) + { + Console.WriteLine(CommandLineOptions.Usage); + return 0; + } + + try + { + Console.WriteLine(BacktestSampleRunner.Run(options)); + return 0; + } + catch (FileNotFoundException ex) + { + Console.Error.WriteLine(ex.Message); + return 1; + } + catch (FormatException ex) + { + Console.Error.WriteLine(ex.Message); + return 1; + } + catch (IOException ex) + { + Console.Error.WriteLine(ex.Message); + return 1; + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Reporting/AsciiEquityCurve.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Reporting/AsciiEquityCurve.cs new file mode 100644 index 00000000..0987834b --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Reporting/AsciiEquityCurve.cs @@ -0,0 +1,160 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Text; + +namespace TechnicalAnalysis.Samples.Backtesting.Reporting; + +/// +/// Renders an equity curve as a compact ASCII chart that survives being pasted into an issue, a log file or a +/// terminal without a graphics stack. +/// +/// +/// The curve is resampled to the requested width by nearest-neighbour, normalised so that the initial capital +/// reads as 100, and drawn with vertical connectors between consecutive columns so that a steep move is a +/// visible line rather than two disconnected dots. +/// +public static class AsciiEquityCurve +{ + private const char Plot = '*'; + private const char Connector = '|'; + private const char Blank = ' '; + private const int LabelWidth = 8; + + /// + /// Renders an equity curve. + /// + /// The curve to draw. An empty curve renders a short placeholder. + /// The number of plot columns, excluding the axis labels. Defaults to 78. + /// The number of plot rows. Defaults to 14. + /// The chart, as lines separated by . + /// is . + /// + /// is below 2 or is below 2. + /// + public static string Render(IReadOnlyList equityCurve, int width = 78, int height = 14) + { + ArgumentNullException.ThrowIfNull(equityCurve); + ArgumentOutOfRangeException.ThrowIfLessThan(width, 2); + ArgumentOutOfRangeException.ThrowIfLessThan(height, 2); + + if (equityCurve.Count == 0) + { + return "(no equity curve: the bar series was empty)"; + } + + double baseline = equityCurve[0].Equity; + if (baseline <= 0.0) + { + baseline = 1.0; + } + + double[] samples = Resample(equityCurve, width, baseline); + double minimum = samples[0]; + double maximum = samples[0]; + foreach (double sample in samples) + { + minimum = Math.Min(minimum, sample); + maximum = Math.Max(maximum, sample); + } + + // A perfectly flat curve would divide by zero; open a symmetric window around it instead. + if (maximum - minimum < 1e-9) + { + maximum += 1.0; + minimum -= 1.0; + } + + char[][] canvas = new char[height][]; + for (int row = 0; row < height; row++) + { + canvas[row] = new char[width]; + Array.Fill(canvas[row], Blank); + } + + int previousRow = -1; + for (int column = 0; column < width; column++) + { + int row = ToRow(samples[column], minimum, maximum, height); + canvas[row][column] = Plot; + + if (previousRow >= 0) + { + int from = Math.Min(previousRow, row); + int to = Math.Max(previousRow, row); + for (int fill = from + 1; fill < to; fill++) + { + canvas[fill][column] = Connector; + } + } + + previousRow = row; + } + + return Compose(canvas, equityCurve, minimum, maximum, width, height); + } + + private static double[] Resample(IReadOnlyList equityCurve, int width, double baseline) + { + double[] samples = new double[width]; + for (int column = 0; column < width; column++) + { + int index = width == 1 + ? equityCurve.Count - 1 + : (int)Math.Round((double)column * (equityCurve.Count - 1) / (width - 1), MidpointRounding.AwayFromZero); + + index = Math.Clamp(index, 0, equityCurve.Count - 1); + samples[column] = equityCurve[index].Equity / baseline * 100.0; + } + + return samples; + } + + private static int ToRow(double value, double minimum, double maximum, int height) + { + double normalised = (value - minimum) / (maximum - minimum); + int fromBottom = (int)Math.Round(normalised * (height - 1), MidpointRounding.AwayFromZero); + return height - 1 - Math.Clamp(fromBottom, 0, height - 1); + } + + private static string Compose( + char[][] canvas, + IReadOnlyList equityCurve, + double minimum, + double maximum, + int width, + int height) + { + StringBuilder builder = new(); + for (int row = 0; row < height; row++) + { + string label = row switch + { + 0 => maximum.ToString("F1", CultureInfo.InvariantCulture), + _ when row == height - 1 => minimum.ToString("F1", CultureInfo.InvariantCulture), + _ when row == height / 2 => ((maximum + minimum) / 2.0).ToString("F1", CultureInfo.InvariantCulture), + _ => string.Empty + }; + + builder.Append(label.PadLeft(LabelWidth)).Append(" |").Append(canvas[row]).Append('\n'); + } + + builder.Append(new string(Blank, LabelWidth)).Append(" +").Append(new string('-', width)).Append('\n'); + + string firstDate = equityCurve[0].Timestamp.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + string lastDate = equityCurve[^1].Timestamp.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture); + string axis = firstDate.Length + lastDate.Length + 1 <= width + ? firstDate + new string(Blank, width - firstDate.Length - lastDate.Length) + lastDate + : firstDate; + + builder.Append(new string(Blank, LabelWidth)).Append(" ").Append(axis); + + return builder + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace("\n", Environment.NewLine, StringComparison.Ordinal); + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Reporting/ConsoleReport.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Reporting/ConsoleReport.cs new file mode 100644 index 00000000..1f1764c3 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Reporting/ConsoleReport.cs @@ -0,0 +1,334 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Text; + +namespace TechnicalAnalysis.Samples.Backtesting.Reporting; + +/// +/// Formats backtest results as plain text: a per-strategy metrics card, a side-by-side comparison table and a +/// trade-log excerpt. +/// +/// +/// Every method returns a string rather than writing to the console, so the output can be asserted in tests, +/// written to a file or piped somewhere else. All formatting is invariant-culture, so the numbers are the +/// same on every machine. +/// +public static class ConsoleReport +{ + private const int MetricLabelWidth = 26; + private const int MetricValueWidth = 16; + + /// + /// Renders the run configuration: capital, frictions, sizing policy and annualisation constant. + /// + /// The options the runs were executed with. + /// The number of bars in the series. + /// A short description of where the bars came from. + /// The formatted header block. + /// + /// or is . + /// + public static string RenderConfiguration(BacktestOptions options, int barCount, string dataSource) + { + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(dataSource); + + StringBuilder builder = new(); + AppendRule(builder, "RUN CONFIGURATION"); + + string sizing = options.Sizing == PositionSizing.FixedFraction + ? string.Format(CultureInfo.InvariantCulture, "fixed fraction {0:P0} of equity", options.PositionFraction) + : string.Format(CultureInfo.InvariantCulture, "fixed cash {0:N0} per position", options.PositionCash); + + AppendField(builder, "Data", dataSource); + AppendField(builder, "Bars", barCount.ToString("N0", CultureInfo.InvariantCulture)); + AppendField(builder, "Initial capital", options.InitialCapital.ToString("N2", CultureInfo.InvariantCulture)); + AppendField(builder, "Commission", string.Format(CultureInfo.InvariantCulture, "{0:0.##} bp per fill", options.CommissionBps)); + AppendField(builder, "Slippage", string.Format(CultureInfo.InvariantCulture, "{0:0.##} bp per fill", options.SlippageBps)); + AppendField(builder, "Position sizing", sizing); + AppendField(builder, "Short selling", options.AllowShort ? "enabled" : "disabled (short signals go flat)"); + AppendField(builder, "Annualisation", string.Format(CultureInfo.InvariantCulture, "{0} bars per year", options.BarsPerYear)); + + return Normalise(builder); + } + + /// + /// Renders a full card for a single run: the strategy's rules, its metrics and its equity curve. + /// + /// The run to report on. + /// The strategy's entry and exit rules, shown under the title. + /// The width of the ASCII equity curve, in characters. + /// The height of the ASCII equity curve, in rows. + /// The formatted card. + /// + /// or is . + /// + public static string RenderStrategyCard(BacktestResult result, string description, int curveWidth = 78, int curveHeight = 12) + { + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(description); + + StringBuilder builder = new(); + AppendRule(builder, result.StrategyName.ToUpperInvariant()); + builder.Append(description).Append('\n').Append('\n'); + builder.Append(RenderMetrics(result.Metrics)).Append('\n'); + builder.Append("Equity curve (initial capital = 100)").Append('\n'); + builder.Append(AsciiEquityCurve.Render(result.EquityCurve, curveWidth, curveHeight)).Append('\n'); + + return Normalise(builder); + } + + /// + /// Renders a two-column metrics table for one run. + /// + /// The metrics to render. + /// The formatted table. + /// is . + public static string RenderMetrics(PerformanceMetrics metrics) + { + ArgumentNullException.ThrowIfNull(metrics); + + StringBuilder builder = new(); + foreach (MetricRow row in MetricRows) + { + builder + .Append(" ") + .Append(row.Label.PadRight(MetricLabelWidth)) + .Append(row.Format(metrics).PadLeft(MetricValueWidth)) + .Append('\n'); + } + + return Normalise(builder); + } + + /// + /// Renders a side-by-side comparison of several runs, one column per strategy, and marks the best value + /// of each row with a leading *. + /// + /// The runs to compare, in display order. Put the baseline last for readability. + /// The formatted comparison table. + /// is . + public static string RenderComparison(IReadOnlyList results) + { + ArgumentNullException.ThrowIfNull(results); + + if (results.Count == 0) + { + return "(nothing to compare)"; + } + + int columnWidth = 2; + foreach (BacktestResult result in results) + { + columnWidth = Math.Max(columnWidth, result.StrategyName.Length); + } + + columnWidth = Math.Max(columnWidth, MetricValueWidth - 2) + 2; + + StringBuilder builder = new(); + AppendRule(builder, "SIDE-BY-SIDE COMPARISON"); + + builder.Append("Metric".PadRight(MetricLabelWidth)); + foreach (BacktestResult result in results) + { + builder.Append(result.StrategyName.PadLeft(columnWidth)); + } + + builder.Append('\n'); + builder.Append(new string('-', MetricLabelWidth + (columnWidth * results.Count))).Append('\n'); + + foreach (MetricRow row in MetricRows) + { + int bestIndex = row.FindBest(results); + + builder.Append(row.Label.PadRight(MetricLabelWidth)); + for (int i = 0; i < results.Count; i++) + { + string cell = row.Format(results[i].Metrics); + string marked = i == bestIndex ? "* " + cell : cell; + builder.Append(marked.PadLeft(columnWidth)); + } + + builder.Append('\n'); + } + + builder.Append('\n').Append(" * marks the best value of the row.").Append('\n'); + + return Normalise(builder); + } + + /// + /// Renders the first few completed round trips of a run, so the reader can sanity-check the fills. + /// + /// The run whose trades to list. + /// The maximum number of trades to show. Defaults to 10. + /// The formatted trade log. + /// is . + public static string RenderTradeLog(BacktestResult result, int maxTrades = 10) + { + ArgumentNullException.ThrowIfNull(result); + + StringBuilder builder = new(); + AppendRule(builder, string.Format(CultureInfo.InvariantCulture, "TRADE LOG - {0}", result.StrategyName.ToUpperInvariant())); + + if (result.Trades.Count == 0) + { + builder.Append(" (no completed round trips)").Append('\n'); + return Normalise(builder); + } + + builder + .Append(" ") + .Append("Side".PadRight(6)) + .Append("Entry".PadRight(13)) + .Append("Exit".PadRight(13)) + .Append("Entry px".PadLeft(11)) + .Append("Exit px".PadLeft(11)) + .Append("Qty".PadLeft(11)) + .Append("Fees".PadLeft(10)) + .Append("Net P&L".PadLeft(13)) + .Append("Bars".PadLeft(7)) + .Append('\n'); + + int shown = Math.Min(maxTrades, result.Trades.Count); + for (int i = 0; i < shown; i++) + { + Trade trade = result.Trades[i]; + builder + .Append(" ") + .Append((trade.IsLong ? "LONG" : "SHORT").PadRight(6)) + .Append(trade.EntryTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture).PadRight(13)) + .Append(trade.ExitTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture).PadRight(13)) + .Append(trade.EntryPrice.ToString("N2", CultureInfo.InvariantCulture).PadLeft(11)) + .Append(trade.ExitPrice.ToString("N2", CultureInfo.InvariantCulture).PadLeft(11)) + .Append(trade.Quantity.ToString("N2", CultureInfo.InvariantCulture).PadLeft(11)) + .Append(trade.Commission.ToString("N2", CultureInfo.InvariantCulture).PadLeft(10)) + .Append(trade.NetProfit.ToString("N2", CultureInfo.InvariantCulture).PadLeft(13)) + .Append(trade.BarsHeld.ToString(CultureInfo.InvariantCulture).PadLeft(7)) + .Append('\n'); + } + + if (result.Trades.Count > shown) + { + builder + .Append(" ... ") + .Append((result.Trades.Count - shown).ToString(CultureInfo.InvariantCulture)) + .Append(" more round trip(s) not shown.") + .Append('\n'); + } + + return Normalise(builder); + } + + private static void AppendRule(StringBuilder builder, string title) + { + builder.Append('\n').Append(new string('=', 100)).Append('\n'); + builder.Append(title).Append('\n'); + builder.Append(new string('=', 100)).Append('\n'); + } + + private static void AppendField(StringBuilder builder, string label, string value) + { + builder.Append(" ").Append((label + ':').PadRight(MetricLabelWidth)).Append(value).Append('\n'); + } + + /// + /// Converts the single '\n' used while composing into the platform newline. Fragments produced by other + /// renderers are already platform-normalised, so any CRLF is folded back first β€” otherwise a second pass + /// on Windows would turn "\r\n" into "\r\r\n". + /// + private static string Normalise(StringBuilder builder) + { + return builder + .ToString() + .Replace("\r\n", "\n", StringComparison.Ordinal) + .Replace("\n", Environment.NewLine, StringComparison.Ordinal); + } + + private static string Percent(double value) + { + return double.IsFinite(value) + ? (value * 100.0).ToString("F2", CultureInfo.InvariantCulture) + " %" + : "n/a"; + } + + private static string Ratio(double value) + { + if (double.IsPositiveInfinity(value)) + { + return "inf"; + } + + return double.IsFinite(value) ? value.ToString("F2", CultureInfo.InvariantCulture) : "n/a"; + } + + private static string Money(double value) + { + return double.IsFinite(value) ? value.ToString("N2", CultureInfo.InvariantCulture) : "n/a"; + } + + private static string Count(double value) + { + return value.ToString("N0", CultureInfo.InvariantCulture); + } + + private static readonly MetricRow[] MetricRows = + [ + new("Final equity", m => Money(m.FinalEquity), m => m.FinalEquity, true), + new("Total return", m => Percent(m.TotalReturn), m => m.TotalReturn, true), + new("CAGR", m => Percent(m.Cagr), m => m.Cagr, true), + new("Annualised volatility", m => Percent(m.AnnualizedVolatility), m => m.AnnualizedVolatility, false), + new("Max drawdown", m => Percent(m.MaxDrawdown), m => m.MaxDrawdown, false), + new("Max DD duration (bars)", m => Count(m.MaxDrawdownDurationBars), m => m.MaxDrawdownDurationBars, false), + new("Sharpe", m => Ratio(m.Sharpe), m => m.Sharpe, true), + new("Sortino", m => Ratio(m.Sortino), m => m.Sortino, true), + new("Calmar", m => Ratio(m.Calmar), m => m.Calmar, true), + new("Trades", m => Count(m.TradeCount), m => m.TradeCount, null), + new("Win rate", m => Percent(m.WinRate), m => m.WinRate, true), + new("Profit factor", m => Ratio(m.ProfitFactor), m => m.ProfitFactor, true), + new("Average win", m => Money(m.AverageWin), m => m.AverageWin, true), + new("Average loss", m => Money(m.AverageLoss), m => m.AverageLoss, true), + new("Expectancy / trade", m => Money(m.Expectancy), m => m.Expectancy, true), + new("Exposure", m => Percent(m.Exposure), m => m.Exposure, null) + ]; + + private sealed record MetricRow( + string Label, + Func Format, + Func Value, + bool? HigherIsBetter) + { + public int FindBest(IReadOnlyList results) + { + if (HigherIsBetter is not { } higherIsBetter || results.Count < 2) + { + return -1; + } + + int bestIndex = -1; + double best = 0.0; + + for (int i = 0; i < results.Count; i++) + { + double value = Value(results[i].Metrics); + if (double.IsNaN(value)) + { + continue; + } + + bool better = bestIndex < 0 || (higherIsBetter ? value > best : value < best); + if (better) + { + bestIndex = i; + best = value; + } + } + + return bestIndex; + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/BollingerBreakoutStrategy.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/BollingerBreakoutStrategy.cs new file mode 100644 index 00000000..54a14f73 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/BollingerBreakoutStrategy.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// A volatility breakout strategy: it trades with a close that escapes the Bollinger envelope, and +/// gives the position back when price returns to the middle band. +/// +/// +/// Entry (long). On the bar where the close breaks above the upper band having been at or below +/// it on the previous bar β€” a move large enough to be unlikely under the recent volatility regime. Filled at +/// the next open. +/// Entry (short). On the bar where the close breaks below the lower band having been at or above +/// it on the previous bar. Downgraded to a flat position when the account forbids shorting. +/// Exit (long). When the close falls back to or below the middle band (the moving average), the +/// breakout is considered failed or spent. +/// Exit (short). When the close rises back to or above the middle band. +/// Warm-up. timePeriod + 1 bars, because entries compare the close to the bands on two +/// consecutive bars. +/// Note. This is the breakout reading of the bands, the opposite of the more common +/// "fade the band" mean-reversion reading. Running it alongside in the +/// sample makes the difference between the two philosophies visible on the same data. +/// +public sealed class BollingerBreakoutStrategy : IStrategy +{ + private readonly int _timePeriod; + private readonly double _deviations; + private BollingerBandSeries? _bands; + + /// + /// Initializes a new instance of the class. + /// + /// The moving-average period of the bands, in bars. Defaults to 20. + /// The number of standard deviations for both bands. Defaults to 2. + /// The period is below 2, or the deviation count is not strictly positive. + public BollingerBreakoutStrategy(int timePeriod = 20, double deviations = 2.0) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 2); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(deviations); + + _timePeriod = timePeriod; + _deviations = deviations; + } + + /// + public string Name => string.Format(CultureInfo.InvariantCulture, "BBands {0}/{1} breakout", _timePeriod, _deviations); + + /// + public string Description => string.Format( + CultureInfo.InvariantCulture, + "Buy a close breaking above the upper {0}-sigma band of BBANDS({1}), sell short below the lower band, exit at the middle band.", + _deviations, + _timePeriod); + + /// + public void Initialize(IIndicatorSource indicators) + { + ArgumentNullException.ThrowIfNull(indicators); + + _bands = indicators.BollingerBands(_timePeriod, _deviations, _deviations); + } + + /// + public Signal Evaluate(IBarWindow bars, Position? position) + { + ArgumentNullException.ThrowIfNull(bars); + + if (_bands is null) + { + throw new InvalidOperationException("Initialize must be called before Evaluate."); + } + + int i = bars.CurrentIndex; + if (i < 1) + { + return Signal.Hold; + } + + if (!_bands.Upper.TryGetPair(i, out double previousUpper, out double currentUpper) + || !_bands.Lower.TryGetPair(i, out double previousLower, out double currentLower) + || !_bands.Middle.TryGetValue(i, out double currentMiddle)) + { + return Signal.Hold; + } + + double previousClose = bars.Ago(1).Close; + double currentClose = bars.Current.Close; + + if (previousClose <= previousUpper && currentClose > currentUpper) + { + return Signal.EnterLong; + } + + if (previousClose >= previousLower && currentClose < currentLower) + { + return Signal.EnterShort; + } + + if (position is null) + { + return Signal.Hold; + } + + bool longFaded = position.IsLong && currentClose <= currentMiddle; + bool shortFaded = position.IsShort && currentClose >= currentMiddle; + + return longFaded || shortFaded ? Signal.Exit : Signal.Hold; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/BuyAndHoldStrategy.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/BuyAndHoldStrategy.cs new file mode 100644 index 00000000..88c0ccc2 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/BuyAndHoldStrategy.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// The baseline every other strategy is measured against: buy once, hold to the end. +/// +/// +/// Entry. A single long signal is emitted at the close of the first bar and filled at the open +/// of the second bar, exactly like any other strategy β€” the baseline pays the same slippage and commission +/// as its competitors, so the comparison is fair. +/// Exit. None. The engine liquidates the position at the close of the last bar when +/// is set, which makes the run exactly one round trip. +/// Its net return is therefore the return of the underlying series from the second bar's open to the last +/// bar's close, less one round trip of costs. +/// Warm-up. None; the strategy uses no indicator. +/// +public sealed class BuyAndHoldStrategy : IStrategy +{ + private bool _entered; + + /// + public string Name => "Buy and hold"; + + /// + public string Description => "Baseline: buy at the second bar's open and hold to the final close, paying one round trip of costs."; + + /// + public void Initialize(IIndicatorSource indicators) + { + ArgumentNullException.ThrowIfNull(indicators); + + _entered = false; + } + + /// + public Signal Evaluate(IBarWindow bars, Position? position) + { + ArgumentNullException.ThrowIfNull(bars); + + if (_entered) + { + return Signal.Hold; + } + + _entered = true; + return Signal.EnterLong; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/IStrategy.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/IStrategy.cs new file mode 100644 index 00000000..5a8f39c3 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/IStrategy.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// A trading rule: it declares the indicators it needs once, then emits a target exposure after each bar +/// closes. +/// +/// +/// +/// The contract is deliberately narrow so that look-ahead bias cannot be written even by accident. A strategy +/// never receives the bar series β€” only an that stops at the current bar and +/// instances bound to the same cursor. Both throw +/// on a future index. +/// +/// +/// A strategy may hold mutable state across bars (a trailing stop, for example), but that state must be reset +/// in , because the same instance may be run over several series. +/// +/// +public interface IStrategy +{ + /// + /// Gets the short display name used in reports, for example "SMA 50/200 crossover". + /// + string Name { get; } + + /// + /// Gets a one-line description of the entry and exit rules, shown above the metrics table. + /// + string Description { get; } + + /// + /// Called once before the first bar. Resolve indicators here and reset any per-run state. + /// + /// The factory used to declare the indicators the strategy needs. + void Initialize(IIndicatorSource indicators); + + /// + /// Called once per bar, after the bar has closed, with the window positioned on that bar. The returned + /// signal is executed at the open of the next bar β€” never on the bar that produced it. + /// + /// The causal price view, positioned on the bar that just closed. + /// The position currently open, or when flat. + /// The target exposure the strategy wants from the next bar onwards. + Signal Evaluate(IBarWindow bars, Position? position); +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/MacdTrendStrategy.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/MacdTrendStrategy.cs new file mode 100644 index 00000000..658fe16b --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/MacdTrendStrategy.cs @@ -0,0 +1,176 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// A MACD trend follower with a volatility-scaled trailing stop, so that a trend that turns is exited on +/// price action rather than waiting for the slower momentum signal to reverse. +/// +/// +/// Entry (long). On the bar where the MACD line closes above its signal line having been at or +/// below it on the previous bar. Filled at the next open. +/// Entry (short). On the bar where the MACD line closes below its signal line having been at or +/// above it on the previous bar. Downgraded to a flat position when the account forbids shorting. +/// Exit β€” ATR trailing stop. While long, the stop is Close - atrMultiple * ATR(period) +/// recomputed on every bar and ratcheted upwards only, so it never loosens. The position is closed on the +/// first bar whose close is at or below the stop. While short the stop is +/// Close + atrMultiple * ATR(period), ratcheted downwards only, and the position is closed on the +/// first close at or above it. The stop is evaluated on closes, not intrabar, because the engine only fills +/// at the next open β€” an intrabar stop would be a look-ahead fiction at this bar granularity. +/// Warm-up. Governed by the MACD (slowPeriod + signalPeriod bars) and by the ATR +/// (atrPeriod bars); the trailing stop simply stays inactive until the ATR has a value. +/// +public sealed class MacdTrendStrategy : IStrategy +{ + private readonly int _fastPeriod; + private readonly int _slowPeriod; + private readonly int _signalPeriod; + private readonly int _atrPeriod; + private readonly double _atrMultiple; + private MacdSeries? _macd; + private IndicatorSeries? _atr; + private double _trailingStop; + private bool _stopArmed; + + /// + /// Initializes a new instance of the class. + /// + /// The fast EMA period of the MACD, in bars. Defaults to 12. + /// The slow EMA period of the MACD, in bars. Defaults to 26. + /// The signal EMA period of the MACD, in bars. Defaults to 9. + /// The ATR period driving the trailing stop, in bars. Defaults to 14. + /// The number of ATRs the trailing stop sits away from the close. Defaults to 3. + /// + /// A period is below 1, the fast period is not shorter than the slow one, or the ATR multiple is not + /// strictly positive. + /// + public MacdTrendStrategy( + int fastPeriod = 12, + int slowPeriod = 26, + int signalPeriod = 9, + int atrPeriod = 14, + double atrMultiple = 3.0) + { + ArgumentOutOfRangeException.ThrowIfLessThan(fastPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(slowPeriod, fastPeriod); + ArgumentOutOfRangeException.ThrowIfLessThan(signalPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(atrPeriod, 1); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(atrMultiple); + + _fastPeriod = fastPeriod; + _slowPeriod = slowPeriod; + _signalPeriod = signalPeriod; + _atrPeriod = atrPeriod; + _atrMultiple = atrMultiple; + } + + /// + public string Name => string.Format( + CultureInfo.InvariantCulture, + "MACD {0}/{1}/{2} + {3}xATR", + _fastPeriod, + _slowPeriod, + _signalPeriod, + _atrMultiple); + + /// + public string Description => string.Format( + CultureInfo.InvariantCulture, + "Trade MACD({0},{1},{2}) signal-line crossings, exited early by a {3}x ATR({4}) trailing stop on closes.", + _fastPeriod, + _slowPeriod, + _signalPeriod, + _atrMultiple, + _atrPeriod); + + /// + public void Initialize(IIndicatorSource indicators) + { + ArgumentNullException.ThrowIfNull(indicators); + + _macd = indicators.Macd(_fastPeriod, _slowPeriod, _signalPeriod); + _atr = indicators.Atr(_atrPeriod); + _trailingStop = 0.0; + _stopArmed = false; + } + + /// + public Signal Evaluate(IBarWindow bars, Position? position) + { + ArgumentNullException.ThrowIfNull(bars); + + if (_macd is null || _atr is null) + { + throw new InvalidOperationException("Initialize must be called before Evaluate."); + } + + int i = bars.CurrentIndex; + double close = bars.Current.Close; + + if (position is null) + { + _stopArmed = false; + } + else if (UpdateTrailingStopAndCheckBreach(position, i, close)) + { + return Signal.Exit; + } + + if (!_macd.Line.TryGetPair(i, out double previousLine, out double currentLine) + || !_macd.Signal.TryGetPair(i, out double previousSignal, out double currentSignal)) + { + return Signal.Hold; + } + + if (previousLine <= previousSignal && currentLine > currentSignal) + { + _stopArmed = false; + return Signal.EnterLong; + } + + if (previousLine >= previousSignal && currentLine < currentSignal) + { + _stopArmed = false; + return Signal.EnterShort; + } + + return Signal.Hold; + } + + private bool UpdateTrailingStopAndCheckBreach(Position position, int barIndex, double close) + { + if (_atr is null || !_atr.TryGetValue(barIndex, out double atr) || atr <= 0.0) + { + return false; + } + + double candidate = position.IsLong + ? close - (_atrMultiple * atr) + : close + (_atrMultiple * atr); + + if (!_stopArmed) + { + _trailingStop = candidate; + _stopArmed = true; + } + else + { + // Ratchet only in the favourable direction: a trailing stop never gives ground. + _trailingStop = position.IsLong + ? Math.Max(_trailingStop, candidate) + : Math.Min(_trailingStop, candidate); + } + + bool breached = position.IsLong ? close <= _trailingStop : close >= _trailingStop; + if (breached) + { + _stopArmed = false; + } + + return breached; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/RsiMeanReversionStrategy.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/RsiMeanReversionStrategy.cs new file mode 100644 index 00000000..58bfacaf --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/RsiMeanReversionStrategy.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// A counter-trend strategy that fades exhaustion: it buys when the Relative Strength Index climbs back out +/// of oversold territory and sells when it falls back out of overbought territory. +/// +/// +/// Entry (long). On the bar where the RSI closes above oversoldLevel having been at or +/// below it on the previous bar β€” the momentum of the sell-off has broken. Filled at the next open. +/// Entry (short). On the bar where the RSI closes below overboughtLevel having been at or +/// above it on the previous bar. Downgraded to a flat position when the account forbids shorting. +/// Exit (long). When the RSI closes at or above exitLevel (50 by default): the reversion +/// to the mean has played out. Long positions are also reversed by a short entry. +/// Exit (short). When the RSI closes at or below exitLevel. +/// Warm-up. timePeriod + 1 bars, because the rules compare the RSI on two consecutive +/// bars. +/// +public sealed class RsiMeanReversionStrategy : IStrategy +{ + private readonly int _timePeriod; + private readonly double _oversoldLevel; + private readonly double _overboughtLevel; + private readonly double _exitLevel; + private IndicatorSeries? _rsi; + + /// + /// Initializes a new instance of the class. + /// + /// The RSI period in bars. Defaults to 14. + /// The RSI level below which the market is considered oversold. Defaults to 30. + /// The RSI level above which the market is considered overbought. Defaults to 70. + /// The RSI level at which an open position is closed. Defaults to 50. + /// + /// The period is below 1, or the levels are not ordered + /// 0 < oversold < overbought < 100, or the exit level lies outside the two. + /// + public RsiMeanReversionStrategy( + int timePeriod = 14, + double oversoldLevel = 30.0, + double overboughtLevel = 70.0, + double exitLevel = 50.0) + { + ArgumentOutOfRangeException.ThrowIfLessThan(timePeriod, 1); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(oversoldLevel); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(overboughtLevel, oversoldLevel); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(overboughtLevel, 100.0); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(exitLevel, oversoldLevel); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(exitLevel, overboughtLevel); + + _timePeriod = timePeriod; + _oversoldLevel = oversoldLevel; + _overboughtLevel = overboughtLevel; + _exitLevel = exitLevel; + } + + /// + public string Name => string.Format(CultureInfo.InvariantCulture, "RSI {0} reversion", _timePeriod); + + /// + public string Description => string.Format( + CultureInfo.InvariantCulture, + "Buy when RSI({0}) crosses back above {1}, sell short when it crosses back below {2}, flatten at {3}.", + _timePeriod, + _oversoldLevel, + _overboughtLevel, + _exitLevel); + + /// + public void Initialize(IIndicatorSource indicators) + { + ArgumentNullException.ThrowIfNull(indicators); + + _rsi = indicators.Rsi(_timePeriod); + } + + /// + public Signal Evaluate(IBarWindow bars, Position? position) + { + ArgumentNullException.ThrowIfNull(bars); + + if (_rsi is null) + { + throw new InvalidOperationException("Initialize must be called before Evaluate."); + } + + int i = bars.CurrentIndex; + if (!_rsi.TryGetPair(i, out double previous, out double current)) + { + return Signal.Hold; + } + + if (previous <= _oversoldLevel && current > _oversoldLevel) + { + return Signal.EnterLong; + } + + if (previous >= _overboughtLevel && current < _overboughtLevel) + { + return Signal.EnterShort; + } + + if (position is null) + { + return Signal.Hold; + } + + bool longTargetReached = position.IsLong && current >= _exitLevel; + bool shortTargetReached = position.IsShort && current <= _exitLevel; + + return longTargetReached || shortTargetReached ? Signal.Exit : Signal.Hold; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/SmaCrossoverStrategy.cs b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/SmaCrossoverStrategy.cs new file mode 100644 index 00000000..58594107 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/Strategies/SmaCrossoverStrategy.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.Strategies; + +/// +/// The classic golden-cross / death-cross trend follower, built on two simple moving averages of the close. +/// +/// +/// Entry (long) β€” golden cross. On the bar where the fast SMA closes above the slow SMA having +/// been at or below it on the previous bar, target a long position. The fill happens at the next bar's open. +/// Entry (short) β€” death cross. On the bar where the fast SMA closes below the slow SMA having +/// been at or above it on the previous bar, target a short position. When the account forbids shorting the +/// engine downgrades this to a flat position, which turns the rule into the familiar long-only crossover. +/// Exit. There is no separate exit rule: the position is always reversed by the opposite cross, +/// so the strategy is continuously in the market once the slow average has warmed up. +/// Warm-up. No signal is produced until both averages have a value on the current and +/// the previous bar, which takes slowPeriod bars. +/// +public sealed class SmaCrossoverStrategy : IStrategy +{ + private readonly int _fastPeriod; + private readonly int _slowPeriod; + private IndicatorSeries? _fast; + private IndicatorSeries? _slow; + + /// + /// Initializes a new instance of the class. + /// + /// The period of the fast moving average, in bars. Defaults to 50. + /// The period of the slow moving average, in bars. Defaults to 200. + /// A period is below 1, or the fast period is not shorter than the slow one. + public SmaCrossoverStrategy(int fastPeriod = 50, int slowPeriod = 200) + { + ArgumentOutOfRangeException.ThrowIfLessThan(fastPeriod, 1); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(slowPeriod, fastPeriod); + + _fastPeriod = fastPeriod; + _slowPeriod = slowPeriod; + } + + /// + public string Name => string.Format(CultureInfo.InvariantCulture, "SMA {0}/{1}", _fastPeriod, _slowPeriod); + + /// + public string Description => string.Format( + CultureInfo.InvariantCulture, + "Long on the golden cross (SMA{0} crossing above SMA{1}), short on the death cross. Always in the market after warm-up.", + _fastPeriod, + _slowPeriod); + + /// + public void Initialize(IIndicatorSource indicators) + { + ArgumentNullException.ThrowIfNull(indicators); + + _fast = indicators.Sma(_fastPeriod); + _slow = indicators.Sma(_slowPeriod); + } + + /// + public Signal Evaluate(IBarWindow bars, Position? position) + { + ArgumentNullException.ThrowIfNull(bars); + + if (_fast is null || _slow is null) + { + throw new InvalidOperationException("Initialize must be called before Evaluate."); + } + + int i = bars.CurrentIndex; + if (!_fast.TryGetPair(i, out double previousFast, out double currentFast) + || !_slow.TryGetPair(i, out double previousSlow, out double currentSlow)) + { + return Signal.Hold; + } + + bool crossedUp = previousFast <= previousSlow && currentFast > currentSlow; + if (crossedUp) + { + return Signal.EnterLong; + } + + bool crossedDown = previousFast >= previousSlow && currentFast < currentSlow; + if (crossedDown) + { + return Signal.EnterShort; + } + + return Signal.Hold; + } +} diff --git a/samples/TechnicalAnalysis.Samples.Backtesting/TechnicalAnalysis.Samples.Backtesting.csproj b/samples/TechnicalAnalysis.Samples.Backtesting/TechnicalAnalysis.Samples.Backtesting.csproj new file mode 100644 index 00000000..61610b88 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.Backtesting/TechnicalAnalysis.Samples.Backtesting.csproj @@ -0,0 +1,25 @@ + + + + Exe + false + TechnicalAnalysis.Samples.Backtesting + + true + + + + + + + + + + + + + diff --git a/samples/TechnicalAnalysis.Samples.RealTime.Client/ClientOptions.cs b/samples/TechnicalAnalysis.Samples.RealTime.Client/ClientOptions.cs new file mode 100644 index 00000000..6caddf62 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime.Client/ClientOptions.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; + +namespace TechnicalAnalysis.Samples.RealTime.Client; + +/// +/// The command line surface of the console client. +/// +/// The absolute URL of the SignalR hub. +/// The symbol to subscribe to, or null to take the server's first symbol. +/// +/// When true, consume the hub's StreamSnapshots method instead of the group push. Both deliver +/// the same snapshots; the flag exists so one binary can exercise both server paths. +/// +/// How long to run before exiting on its own, or null to run until Ctrl+C. +public sealed record ClientOptions(Uri HubUrl, string? Symbol, bool UseStreaming, TimeSpan? Duration) +{ + private const string DefaultServer = "http://localhost:5199"; + private const string HubPath = "/hubs/indicators"; + + /// The usage text shown for --help and for a bad argument. + public const string Usage = """ + Usage: dotnet run [-- ] [options] + + Server root or full hub URL. Default: http://localhost:5199 + --symbol Symbol to follow. Default: the server's first symbol. + --stream Use the hub's streaming method instead of the group push. + --seconds Exit after n seconds instead of running until Ctrl+C. + --help Show this text. + """; + + /// + /// Parses the command line. + /// + /// The raw arguments. + /// The parsed options, when parsing succeeded. + /// The reason parsing failed, otherwise null. + /// when the arguments are usable. + public static bool TryParse(string[] args, out ClientOptions options, out string? error) + { + options = null!; + error = null; + + string server = DefaultServer; + string? symbol = null; + bool streaming = false; + TimeSpan? duration = null; + + for (int i = 0; i < args.Length; i++) + { + string argument = args[i]; + switch (argument) + { + case "--help" or "-h": + error = string.Empty; + return false; + + case "--stream": + streaming = true; + break; + + case "--symbol": + if (!TryTakeValue(args, ref i, out symbol)) + { + error = "--symbol needs a value."; + return false; + } + + break; + + case "--seconds": + if (!TryTakeValue(args, ref i, out string? seconds) + || !double.TryParse(seconds, CultureInfo.InvariantCulture, out double parsed) + || parsed <= 0) + { + error = "--seconds needs a positive number."; + return false; + } + + duration = TimeSpan.FromSeconds(parsed); + break; + + default: + if (argument.StartsWith('-')) + { + error = $"Unknown option '{argument}'."; + return false; + } + + server = argument; + break; + } + } + + if (!Uri.TryCreate(server, UriKind.Absolute, out Uri? uri)) + { + error = $"'{server}' is not an absolute URL."; + return false; + } + + // Accept either a server root or a full hub URL, so both of these work: + // dotnet run -- http://localhost:5199 + // dotnet run -- http://localhost:5199/hubs/indicators + if (!uri.AbsolutePath.TrimEnd('/').EndsWith(HubPath, StringComparison.OrdinalIgnoreCase)) + { + uri = new Uri(uri, HubPath); + } + + options = new ClientOptions(uri, symbol, streaming, duration); + return true; + } + + private static bool TryTakeValue(string[] args, ref int index, out string? value) + { + if (index + 1 >= args.Length) + { + value = null; + return false; + } + + index++; + value = args[index]; + return true; + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime.Client/IndicatorSnapshotDto.cs b/samples/TechnicalAnalysis.Samples.RealTime.Client/IndicatorSnapshotDto.cs new file mode 100644 index 00000000..f0697419 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime.Client/IndicatorSnapshotDto.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.RealTime.Client; + +/// +/// The client's copy of the server's snapshot contract. +/// +/// +/// The client deliberately does not reference the server project. A wire contract that only compiles +/// because both ends share an assembly is not a wire contract, and copying the shape here is what a real +/// third-party consumer would do. The SignalR JSON protocol matches names case-insensitively, so these +/// PascalCase members bind to the camelCase names on the wire. +/// +/// The instrument. +/// The bar's UTC open instant. +/// The bar's close. +/// The per-symbol bar counter, used to spot dropped frames. +/// Fast simple moving average, or null while warming up. +/// Slow simple moving average, or null while warming up. +/// Exponential moving average, or null while warming up. +/// Relative strength index, or null while warming up. +/// MACD line, or null while warming up. +/// MACD signal line, or null while warming up. +/// MACD histogram, or null while warming up. +/// Upper Bollinger band, or null while warming up. +/// Middle Bollinger band, or null while warming up. +/// Lower Bollinger band, or null while warming up. +/// Average true range, or null while warming up. +/// The server's combined read for this bar. +/// Bars currently in the server's rolling window. +/// Bars the slowest indicator needs before it prints. +public sealed record IndicatorSnapshotDto( + string Symbol, + DateTimeOffset Timestamp, + decimal Close, + long Sequence, + double? SmaFast, + double? SmaSlow, + double? Ema, + double? Rsi, + double? Macd, + double? MacdSignal, + double? MacdHistogram, + double? BollingerUpper, + double? BollingerMiddle, + double? BollingerLower, + double? Atr, + string Signal, + int BarsInWindow, + int BarsRequired); diff --git a/samples/TechnicalAnalysis.Samples.RealTime.Client/Program.cs b/samples/TechnicalAnalysis.Samples.RealTime.Client/Program.cs new file mode 100644 index 00000000..2b135e5f --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime.Client/Program.cs @@ -0,0 +1,141 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using Microsoft.AspNetCore.SignalR.Client; +using TechnicalAnalysis.Samples.RealTime.Client; + +if (!ClientOptions.TryParse(args, out ClientOptions options, out string? parseError)) +{ + if (!string.IsNullOrEmpty(parseError)) + { + Console.Error.WriteLine(parseError); + Console.Error.WriteLine(); + } + + Console.Error.WriteLine(ClientOptions.Usage); + return string.IsNullOrEmpty(parseError) ? 0 : 64; +} + +using CancellationTokenSource shutdown = new(); + +// Ctrl+C cancels rather than kills, so the hub connection gets to say goodbye and the terminal gets its +// cursor back. A second Ctrl+C falls through to the runtime default and ends the process outright. +Console.CancelKeyPress += (_, eventArgs) => +{ + if (!shutdown.IsCancellationRequested) + { + eventArgs.Cancel = true; + shutdown.Cancel(); + } +}; + +if (options.Duration is { } duration) +{ + shutdown.CancelAfter(duration); +} + +SnapshotRenderer renderer = new(); + +HubConnection connection = new HubConnectionBuilder() + .WithUrl(options.HubUrl) + .WithAutomaticReconnect([TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10)]) + .Build(); + +string? symbol = options.Symbol; + +connection.Closed += _ => +{ + renderer.SetStatus("disconnected"); + return Task.CompletedTask; +}; + +connection.Reconnecting += _ => +{ + renderer.SetStatus("reconnecting"); + return Task.CompletedTask; +}; + +connection.Reconnected += async _ => +{ + renderer.SetStatus("streaming"); + + // Group membership does not survive a reconnect: the server sees a brand new connection id, so the + // subscription has to be re-established or the stream silently goes quiet. + if (!options.UseStreaming && symbol is not null) + { + await connection.InvokeAsync("Subscribe", symbol, shutdown.Token); + } +}; + +int exitCode = 0; + +try +{ + renderer.SetStatus("connecting"); + await connection.StartAsync(shutdown.Token); + + IReadOnlyList symbols = await connection.InvokeAsync>("GetSymbols", shutdown.Token); + if (symbols.Count == 0) + { + throw new InvalidOperationException("The server publishes no symbols."); + } + + symbol = symbol is null + ? symbols[0] + : symbols.FirstOrDefault(known => string.Equals(known, symbol, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidOperationException(string.Create( + CultureInfo.InvariantCulture, + $"Unknown symbol '{symbol}'. This server publishes: {string.Join(", ", symbols)}.")); + + renderer.SetStatus(string.Create( + CultureInfo.InvariantCulture, + $"streaming {symbol} from {options.HubUrl} ({(options.UseStreaming ? "streaming method" : "group push")})")); + + if (options.UseStreaming) + { + // Streaming transport: one server-to-client sequence scoped to this call. + IAsyncEnumerable stream = + connection.StreamAsync("StreamSnapshots", symbol, shutdown.Token); + + await foreach (IndicatorSnapshotDto snapshot in stream.WithCancellation(shutdown.Token)) + { + renderer.Render(snapshot); + } + } + else + { + // Push transport: join the symbol's group and handle the client method. + using IDisposable handler = connection.On("snapshot", renderer.Render); + await connection.InvokeAsync("Subscribe", symbol, shutdown.Token); + await Task.Delay(Timeout.InfiniteTimeSpan, shutdown.Token); + } +} +catch (OperationCanceledException) +{ + // Ctrl+C, or --seconds elapsed. Both are a clean exit. +} +catch (Exception exception) when (exception is HttpRequestException or InvalidOperationException) +{ + renderer.Restore(); + Console.Error.WriteLine(string.Create( + CultureInfo.InvariantCulture, + $"Could not stream from {options.HubUrl}: {exception.Message}")); + Console.Error.WriteLine("Is the server running? Start it with: dotnet run --project samples/TechnicalAnalysis.Samples.RealTime"); + exitCode = 69; +} +finally +{ + await connection.DisposeAsync(); + renderer.Restore(); +} + +if (exitCode == 0) +{ + Console.WriteLine("Disconnected."); +} + +return exitCode; diff --git a/samples/TechnicalAnalysis.Samples.RealTime.Client/SnapshotRenderer.cs b/samples/TechnicalAnalysis.Samples.RealTime.Client/SnapshotRenderer.cs new file mode 100644 index 00000000..f52df2fd --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime.Client/SnapshotRenderer.cs @@ -0,0 +1,226 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using System.Text; + +namespace TechnicalAnalysis.Samples.RealTime.Client; + +/// +/// Draws incoming snapshots, either as a table that updates in place or as one line per bar. +/// +/// +/// Which one it picks depends on . A terminal gets the table, +/// repainted from the top on every bar. A pipe or a log file gets append-only lines, because cursor +/// escapes in a captured log are unreadable and because a build server wants a diffable transcript. +/// +public sealed class SnapshotRenderer +{ + private const string Home = "\u001b[H\u001b[J"; + private const string HideCursor = "\u001b[?25l"; + private const string ShowCursor = "\u001b[?25h"; + private const string Reset = "\u001b[0m"; + private const string Dim = "\u001b[2m"; + private const string Bold = "\u001b[1m"; + private const string Green = "\u001b[32m"; + private const string Red = "\u001b[31m"; + private const string Yellow = "\u001b[33m"; + + private readonly bool _interactive; + private readonly StringBuilder _buffer = new(); + private decimal? _firstClose; + private long _lastSequence = -1; + private long _dropped; + private string _status = "connecting"; + + /// + /// Initializes a new instance of the class. + /// + public SnapshotRenderer() + { + _interactive = !Console.IsOutputRedirected; + if (_interactive) + { + Console.Write(HideCursor); + } + } + + /// + /// Restores the terminal. Called on the way out, including after Ctrl+C. + /// + public void Restore() + { + if (_interactive) + { + Console.Write(ShowCursor); + Console.WriteLine(); + } + } + + /// + /// Records a connection state change so the next frame shows it. + /// + /// The state to display, for example streaming or reconnecting. + public void SetStatus(string status) + { + _status = status; + if (!_interactive) + { + Console.WriteLine(string.Create(CultureInfo.InvariantCulture, $"[{Timestamp()}] {status}")); + } + } + + /// + /// Renders one snapshot. + /// + /// The snapshot to render. + public void Render(IndicatorSnapshotDto snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + + _firstClose ??= snapshot.Close; + + // The server's queues drop the oldest frame rather than stall the feed, so a gap in the sequence + // is expected behaviour rather than a bug. Counting it is how a client notices. + if (_lastSequence >= 0 && snapshot.Sequence > _lastSequence + 1) + { + _dropped += snapshot.Sequence - _lastSequence - 1; + } + + _lastSequence = snapshot.Sequence; + + if (_interactive) + { + RenderTable(snapshot); + } + else + { + Console.WriteLine(RenderLine(snapshot)); + } + } + + /// + /// Builds the append-only form: everything about one bar on one greppable line. + /// + /// The snapshot to render. + /// The formatted line. + public static string RenderLine(IndicatorSnapshotDto snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + + return string.Create( + CultureInfo.InvariantCulture, + $"{snapshot.Timestamp.ToLocalTime():HH:mm:ss} {snapshot.Symbol,-7} #{snapshot.Sequence,-3} " + + $"close={snapshot.Close,8:0.00} " + + $"smaFast={Number(snapshot.SmaFast, "0.00"),8} " + + $"smaSlow={Number(snapshot.SmaSlow, "0.00"),8} " + + $"ema={Number(snapshot.Ema, "0.00"),8} " + + $"rsi={Number(snapshot.Rsi, "0.0"),6} " + + $"macd={Number(snapshot.Macd, "0.0000"),8} " + + $"macdSignal={Number(snapshot.MacdSignal, "0.0000"),8} " + + $"macdHist={Number(snapshot.MacdHistogram, "0.0000"),8} " + + $"bbUpper={Number(snapshot.BollingerUpper, "0.00"),8} " + + $"bbLower={Number(snapshot.BollingerLower, "0.00"),8} " + + $"atr={Number(snapshot.Atr, "0.0000"),8} " + + $"signal={snapshot.Signal,-10} " + + $"warm={snapshot.BarsInWindow}/{snapshot.BarsRequired}"); + } + + private static string Number(double? value, string format) + => value.HasValue ? value.Value.ToString(format, CultureInfo.InvariantCulture) : "null"; + + private static string Timestamp() => DateTime.Now.ToString("HH:mm:ss", CultureInfo.InvariantCulture); + + private void RenderTable(IndicatorSnapshotDto snapshot) + { + decimal change = _firstClose is { } first && first > 0m + ? (snapshot.Close - first) / first * 100m + : 0m; + + string rule = new('─', 62); + + _buffer.Clear(); + _buffer.Append(Home); + _buffer.Append(Bold).Append(" TaLibStandard Β· live tape").Append(Reset); + _buffer.Append(Dim).Append(" ").Append(snapshot.Symbol).Append(" ").Append(_status).Append(Reset).Append('\n'); + _buffer.Append(" ").Append(rule).Append('\n'); + + _buffer.Append(string.Create( + CultureInfo.InvariantCulture, + $" bar {snapshot.Timestamp.ToLocalTime():HH:mm:ss} seq {snapshot.Sequence,-6} close {snapshot.Close,10:0.00} ")) + .Append(change >= 0 ? Green : Red) + .Append(string.Create(CultureInfo.InvariantCulture, $"{(change >= 0 ? "+" : string.Empty)}{change:0.00}%")) + .Append(Reset) + .Append('\n'); + + _buffer.Append(Dim) + .Append(string.Create( + CultureInfo.InvariantCulture, + $" window {snapshot.BarsInWindow}/{snapshot.BarsRequired} bars until every indicator is warm")) + .Append(_dropped > 0 ? string.Create(CultureInfo.InvariantCulture, $" Β· {_dropped} frame(s) dropped") : string.Empty) + .Append(Reset) + .Append('\n'); + + _buffer.Append(" ").Append(rule).Append('\n'); + + AppendPair(snapshot.SmaFast, "SMA fast", "0.00", snapshot.Macd, "MACD", "0.0000"); + AppendPair(snapshot.SmaSlow, "SMA slow", "0.00", snapshot.MacdSignal, "MACD signal", "0.0000"); + AppendPair(snapshot.Ema, "EMA", "0.00", snapshot.MacdHistogram, "MACD hist", "0.0000"); + AppendPair(snapshot.Rsi, "RSI", "0.0", snapshot.BollingerUpper, "BB upper", "0.00"); + AppendPair(snapshot.Atr, "ATR", "0.0000", snapshot.BollingerMiddle, "BB middle", "0.00"); + AppendPair(null, string.Empty, "0.00", snapshot.BollingerLower, "BB lower", "0.00"); + + _buffer.Append(" ").Append(rule).Append('\n'); + _buffer.Append(" signal ") + .Append(SignalColour(snapshot.Signal)) + .Append(snapshot.Signal.ToUpperInvariant()) + .Append(Reset) + .Append('\n'); + _buffer.Append(Dim).Append(" a dashed value has not warmed up: the server sends null, not zero.").Append(Reset).Append('\n'); + _buffer.Append(Dim).Append(" Ctrl+C to leave.").Append(Reset).Append('\n'); + + Console.Out.Write(_buffer.ToString()); + Console.Out.Flush(); + } + + private void AppendPair(double? left, string leftLabel, string leftFormat, double? right, string rightLabel, string rightFormat) + { + _buffer.Append(" "); + AppendCell(left, leftLabel, leftFormat); + _buffer.Append(" "); + AppendCell(right, rightLabel, rightFormat); + _buffer.Append('\n'); + } + + private void AppendCell(double? value, string label, string format) + { + if (label.Length == 0) + { + _buffer.Append(new string(' ', 26)); + return; + } + + _buffer.Append(Dim).Append(label.PadRight(12)).Append(Reset); + + if (value.HasValue) + { + _buffer.Append(value.Value.ToString(format, CultureInfo.InvariantCulture).PadLeft(12)); + } + else + { + _buffer.Append(Yellow).Append("–––".PadLeft(12)).Append(Reset); + } + + _buffer.Append(new string(' ', 2)); + } + + private static string SignalColour(string signal) => signal switch + { + "Bullish" or "Oversold" => Green, + "Bearish" or "Overbought" => Red, + _ => Dim + }; +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime.Client/TechnicalAnalysis.Samples.RealTime.Client.csproj b/samples/TechnicalAnalysis.Samples.RealTime.Client/TechnicalAnalysis.Samples.RealTime.Client.csproj new file mode 100644 index 00000000..75827b3e --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime.Client/TechnicalAnalysis.Samples.RealTime.Client.csproj @@ -0,0 +1,13 @@ + + + + Exe + false + TechnicalAnalysis.Samples.RealTime.Client + + + + + + + diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Configuration/RealTimeOptions.cs b/samples/TechnicalAnalysis.Samples.RealTime/Configuration/RealTimeOptions.cs new file mode 100644 index 00000000..6a6c8d4c --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Configuration/RealTimeOptions.cs @@ -0,0 +1,128 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.RealTime.Configuration; + +/// +/// Everything the sample can be tuned with, bound from the RealTime section of appsettings.json. +/// +/// +/// Every value can also be overridden on the command line, for example +/// dotnet run -- --RealTime:BarSeconds=1 --RealTime:TickIntervalMilliseconds=100, which is how the +/// README's quick demo shortens the warm-up. +/// +public sealed class RealTimeOptions +{ + /// The configuration section this class binds to. + public const string SectionName = "RealTime"; + + /// + /// Gets or sets the symbols the synthetic feed publishes. The first entry is the page's default. + /// + /// + /// This deliberately starts empty and is filled from appsettings.json. The configuration binder + /// appends to a collection that already has items rather than replacing it, so a code-side + /// default of three symbols plus three in configuration binds to six β€” each one duplicated, each with + /// its own price walk. Startup validation rejects an empty list, so a missing configuration file is a + /// clear failure rather than a silent one. + /// + public string[] Symbols { get; set; } = []; + + /// + /// Gets or sets the interval between synthetic ticks, in milliseconds. + /// + public int TickIntervalMilliseconds { get; set; } = 250; + + /// + /// Gets or sets the bar period, in seconds. Ticks are folded into bars aligned to this period. + /// + public int BarSeconds { get; set; } = 5; + + /// + /// Gets or sets the seed for the synthetic price walk. The same seed always produces the same price + /// path, which is what makes this sample reproducible offline. + /// + public int RandomSeed { get; set; } = 20240613; + + /// + /// Gets or sets the number of closed bars the rolling indicator window keeps per symbol. + /// + /// + /// Must comfortably exceed the slowest indicator's lookback. See + /// RollingIndicatorEngine for why "comfortably" and not "just barely". + /// + public int WindowSize { get; set; } = 256; + + /// + /// Gets or sets how many items a single subscriber may fall behind by before the oldest ones are + /// dropped. See BoundedFanout{T} for the backpressure policy this feeds. + /// + public int SubscriberQueueCapacity { get; set; } = 64; + + /// + /// Gets or sets the per-tick log-return standard deviation of the synthetic walk. Larger values make + /// the chart livelier; 0.0015 is roughly "a busy small cap". + /// + public double Volatility { get; set; } = 0.0015; + + /// + /// Gets or sets the strength of the pull back towards a symbol's base price, per tick. Zero gives a + /// pure random walk, which tends to drift off screen; a small positive value keeps the series in a + /// readable range without flattening it. + /// + public double MeanReversion { get; set; } = 0.0025; + + /// + /// Gets or sets the indicator periods used by the rolling engine. + /// + public IndicatorOptions Indicators { get; set; } = new(); +} + +/// +/// The indicator periods the rolling engine recomputes on every closed bar. +/// +/// +/// Every period is passed straight through to TAMath, which rejects anything below 2 with +/// RetCode.BadParam. Options validation catches that at startup rather than per bar. +/// +public sealed class IndicatorOptions +{ + /// Gets or sets the fast simple moving average period. + public int SmaFastPeriod { get; set; } = 10; + + /// Gets or sets the slow simple moving average period. + public int SmaSlowPeriod { get; set; } = 30; + + /// Gets or sets the exponential moving average period. + public int EmaPeriod { get; set; } = 20; + + /// Gets or sets the relative strength index period. + public int RsiPeriod { get; set; } = 14; + + /// Gets or sets the MACD fast period. + public int MacdFastPeriod { get; set; } = 12; + + /// Gets or sets the MACD slow period. + public int MacdSlowPeriod { get; set; } = 26; + + /// Gets or sets the MACD signal period. + public int MacdSignalPeriod { get; set; } = 9; + + /// Gets or sets the Bollinger band period. + public int BollingerPeriod { get; set; } = 20; + + /// Gets or sets the Bollinger band standard deviation multiplier. + public double BollingerDeviations { get; set; } = 2.0; + + /// Gets or sets the average true range period. + public int AtrPeriod { get; set; } = 14; + + /// Gets or sets the RSI level at or above which the signal reads overbought. + public double RsiOverbought { get; set; } = 70.0; + + /// Gets or sets the RSI level at or below which the signal reads oversold. + public double RsiOversold { get; set; } = 30.0; +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Bar.cs b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Bar.cs new file mode 100644 index 00000000..e564d5ad --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Bar.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.RealTime.Contracts; + +/// +/// A closed OHLCV bar aggregated from a run of values. +/// +/// The instrument the bar belongs to. +/// The UTC instant the bar period opened. A bar labelled 12:00:05 with a five second +/// period covers the half-open interval [12:00:05, 12:00:10). +/// The first traded price of the period. +/// The highest traded price of the period. +/// The lowest traded price of the period. +/// The last traded price of the period. +/// The summed traded size of the period. +/// How many ticks were folded into the bar. Useful when diagnosing a thin period. +public sealed record Bar( + string Symbol, + DateTimeOffset Timestamp, + decimal Open, + decimal High, + decimal Low, + decimal Close, + long Volume, + int TickCount); diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Contracts/IndicatorSnapshot.cs b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/IndicatorSnapshot.cs new file mode 100644 index 00000000..c689ac1a --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/IndicatorSnapshot.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.RealTime.Contracts; + +/// +/// The latest value of every indicator the sample tracks, for one closed bar of one symbol. +/// +/// +/// Every indicator value is nullable and every null means the same thing: that indicator has not +/// produced a value for this bar yet. A null is never replaced by zero and never by the previous +/// bar's value, because both of those lies are indistinguishable from a real reading downstream. +/// +/// The instrument the snapshot describes. +/// The UTC open instant of the bar the snapshot was computed from. +/// The close of that bar. +/// A monotonically increasing bar counter per symbol, starting at 1. Lets a client +/// detect frames it dropped, which a bounded stream is allowed to do. +/// Latest fast simple moving average, or null while warming up. +/// Latest slow simple moving average, or null while warming up. +/// Latest exponential moving average, or null while warming up. +/// Latest relative strength index, or null while warming up. +/// Latest MACD line, or null while warming up. +/// Latest MACD signal line, or null while warming up. +/// Latest MACD histogram, or null while warming up. +/// Latest upper Bollinger band, or null while warming up. +/// Latest middle Bollinger band, or null while warming up. +/// Latest lower Bollinger band, or null while warming up. +/// Latest average true range, or null while warming up. +/// The combined read for this bar. while warming up. +/// How many bars the rolling window currently holds. +/// How many bars the slowest configured indicator needs before it can produce a value. +public sealed record IndicatorSnapshot( + string Symbol, + DateTimeOffset Timestamp, + decimal Close, + long Sequence, + double? SmaFast, + double? SmaSlow, + double? Ema, + double? Rsi, + double? Macd, + double? MacdSignal, + double? MacdHistogram, + double? BollingerUpper, + double? BollingerMiddle, + double? BollingerLower, + double? Atr, + Signal Signal, + int BarsInWindow, + int BarsRequired) +{ + /// + /// Gets a value indicating whether every tracked indicator has produced a value for this bar. + /// + public bool IsWarmedUp => + SmaFast.HasValue + && SmaSlow.HasValue + && Ema.HasValue + && Rsi.HasValue + && Macd.HasValue + && MacdSignal.HasValue + && MacdHistogram.HasValue + && BollingerUpper.HasValue + && Atr.HasValue; +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Contracts/RealTimeJsonContext.cs b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/RealTimeJsonContext.cs new file mode 100644 index 00000000..44d28965 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/RealTimeJsonContext.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Text.Json.Serialization; + +namespace TechnicalAnalysis.Samples.RealTime.Contracts; + +/// +/// The System.Text.Json source generated contract for every type that crosses the wire. +/// +/// +/// +/// Source generation removes the reflection-based serializer from the hot path, which keeps the sample +/// trim and native-AOT friendly. The generated metadata is wired into three places in +/// Program.cs: the minimal API endpoints, the SignalR JSON hub protocol, and the raw WebSocket +/// handler. +/// +/// +/// The naming policy is camel case so the generated names line up with the SignalR JSON protocol +/// defaults and with idiomatic JavaScript on the page. Nulls are written explicitly rather than omitted: +/// an absent indicator is the most important thing a frame can say, so it is stated rather than implied. +/// +/// +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] +[JsonSerializable(typeof(Tick))] +[JsonSerializable(typeof(Bar))] +[JsonSerializable(typeof(IndicatorSnapshot))] +[JsonSerializable(typeof(StreamMessage))] +[JsonSerializable(typeof(SessionInfo))] +[JsonSerializable(typeof(IndicatorPeriods))] +[JsonSerializable(typeof(HealthResponse))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(string[]))] +public sealed partial class RealTimeJsonContext : JsonSerializerContext; diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Signal.cs b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Signal.cs new file mode 100644 index 00000000..63ca3ed9 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Signal.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Text.Json.Serialization; + +namespace TechnicalAnalysis.Samples.RealTime.Contracts; + +/// +/// The coarse read the sample derives from a warmed-up . +/// +/// +/// This is deliberately simplistic: it exists to show how several indicator outputs combine into one +/// decision, not to be traded. It is serialised as a string so the browser and console clients can render +/// it without sharing an enum definition. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum Signal +{ + /// No opinion: the inputs disagree, or not enough indicators have warmed up yet. + Neutral = 0, + + /// Fast average above slow average and the MACD histogram positive. + Bullish = 1, + + /// Fast average below slow average and the MACD histogram negative. + Bearish = 2, + + /// RSI at or above the configured overbought threshold. + Overbought = 3, + + /// RSI at or below the configured oversold threshold. + Oversold = 4 +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Contracts/StreamMessage.cs b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/StreamMessage.cs new file mode 100644 index 00000000..6108c428 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/StreamMessage.cs @@ -0,0 +1,126 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.RealTime.Contracts; + +/// +/// The envelope every frame on the raw /ws/indicators socket uses. +/// +/// +/// SignalR gives clients a method name for free; a raw socket does not, so the sample carries a +/// discriminator instead. Exactly one payload property is populated for a given . +/// +/// The frame kind: session, snapshot or error. +/// Populated for session frames, sent once right after the upgrade. +/// Populated for snapshot frames, one per closed bar. +/// Populated for error frames, describing why the socket is about to close. +public sealed record StreamMessage( + string Type, + SessionInfo? Session = null, + IndicatorSnapshot? Snapshot = null, + string? Error = null) +{ + /// The value of the opening handshake frame. + public const string SessionType = "session"; + + /// The value of a per-bar indicator frame. + public const string SnapshotType = "snapshot"; + + /// The value of a terminal error frame. + public const string ErrorType = "error"; + + /// Creates the opening handshake frame. + /// The session description to send. + /// A session frame. + public static StreamMessage ForSession(SessionInfo session) => new(SessionType, Session: session); + + /// Creates a per-bar indicator frame. + /// The snapshot to send. + /// A snapshot frame. + public static StreamMessage ForSnapshot(IndicatorSnapshot snapshot) => new(SnapshotType, Snapshot: snapshot); + + /// Creates a terminal error frame. + /// A human readable reason. + /// An error frame. + public static StreamMessage ForError(string error) => new(ErrorType, Error: error); +} + +/// +/// What the server tells a freshly connected socket about itself, so the page can label its axes and +/// legends from configuration instead of hard-coding periods. +/// +/// The symbol this socket is subscribed to. +/// Every symbol the server publishes. +/// The bar period, in seconds. +/// The interval between synthetic ticks, in milliseconds. +/// The number of bars the rolling indicator window keeps. +/// The configured indicator periods. +public sealed record SessionInfo( + string Symbol, + IReadOnlyList Symbols, + int BarSeconds, + int TickIntervalMilliseconds, + int WindowSize, + IndicatorPeriods Periods); + +/// +/// The indicator periods in force on the server. +/// +/// Fast simple moving average period. +/// Slow simple moving average period. +/// Exponential moving average period. +/// Relative strength index period. +/// MACD fast period. +/// MACD slow period. +/// MACD signal period. +/// Bollinger band period. +/// Bollinger band standard deviation multiplier. +/// Average true range period. +public sealed record IndicatorPeriods( + int SmaFast, + int SmaSlow, + int Ema, + int Rsi, + int MacdFast, + int MacdSlow, + int MacdSignal, + int Bollinger, + double BollingerDeviations, + int Atr); + +/// +/// The payload of GET /health. +/// +/// Always healthy once the host is serving. +/// The server clock, so a client can spot a badly skewed machine. +/// Seconds since the process started serving. +/// Every symbol the server publishes. +/// Ticks generated by the synthetic feed since start. +/// Bars closed by the aggregators since start. +/// Live tick subscribers, which should equal the number of running pipelines. +/// +/// Live snapshot channel subscribers across all symbols: raw WebSocket clients and hub streaming +/// callers. It does not cover the SignalR push transport, whose subscribers hold a group membership rather +/// than a channel β€” see . +/// +/// +/// Live SignalR push subscriptions across all symbols, one per connection per subscribed symbol. +/// +/// +/// Bars the SignalR push transport discarded because a group did not accept them in time. Non-zero means a +/// subscriber stopped reading; the other transports are unaffected. +/// +public sealed record HealthResponse( + string Status, + DateTimeOffset ServerTimeUtc, + double UptimeSeconds, + IReadOnlyList Symbols, + long TicksPublished, + long BarsClosed, + int TickSubscribers, + int SnapshotSubscribers, + int GroupSubscribers, + long PushesDropped); diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Tick.cs b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Tick.cs new file mode 100644 index 00000000..65845a5e --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Contracts/Tick.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.RealTime.Contracts; + +/// +/// A single trade print produced by the synthetic market data feed. +/// +/// The instrument the print belongs to, for example ACME. +/// The UTC instant the print was produced. +/// The traded price. Money is modelled with so the wire value is exact. +/// The traded size, in whole units. +public sealed record Tick(string Symbol, DateTimeOffset Timestamp, decimal Price, long Volume); diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Diagnostics/SampleLog.cs b/samples/TechnicalAnalysis.Samples.RealTime/Diagnostics/SampleLog.cs new file mode 100644 index 00000000..ade57c87 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Diagnostics/SampleLog.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using TechnicalAnalysis.Samples.RealTime.Contracts; + +namespace TechnicalAnalysis.Samples.RealTime.Diagnostics; + +/// +/// Every log message the sample emits, declared once and generated by the logging source generator. +/// +/// +/// Source generated logging keeps message templates in one place, allocates nothing when a level is +/// disabled, and skips the boxing that the LogInformation(string, params object?[]) overloads do on +/// every call. On a per-bar path that matters; in a sample it also documents exactly what the server says +/// about itself. +/// +internal static partial class SampleLog +{ + [LoggerMessage( + EventId = 1000, + Level = LogLevel.Information, + Message = "Synthetic feed starting for {SymbolCount} symbol(s) at one tick every {IntervalMilliseconds} ms.")] + public static partial void FeedStarting(ILogger logger, int symbolCount, double intervalMilliseconds); + + [LoggerMessage(EventId = 1001, Level = LogLevel.Information, Message = "Synthetic feed stopped after {TickCount} tick(s).")] + public static partial void FeedStopped(ILogger logger, long tickCount); + + [LoggerMessage( + EventId = 1100, + Level = LogLevel.Information, + Message = "Indicator pipeline running: {BarSeconds}s bars, {WindowSize}-bar window, " + + "{BarsRequired} bars until every indicator is warm.")] + public static partial void PipelineStarted(ILogger logger, int barSeconds, int windowSize, int barsRequired); + + [LoggerMessage(EventId = 1101, Level = LogLevel.Information, Message = "Indicator pipeline stopped after {BarCount} bar(s).")] + public static partial void PipelineStopped(ILogger logger, long barCount); + + [LoggerMessage( + EventId = 1102, + Level = LogLevel.Debug, + Message = "{Symbol} bar {Sequence} closed at {Close} ({BarsInWindow}/{BarsRequired} bars, signal {Signal}).")] + public static partial void BarClosed( + ILogger logger, + string symbol, + long sequence, + decimal close, + int barsInWindow, + int barsRequired, + Signal signal); + + [LoggerMessage( + EventId = 1103, + Level = LogLevel.Warning, + Message = "SignalR group push for {Symbol} bar {Sequence} exceeded {TimeoutMilliseconds} ms and was dropped; " + + "a subscriber has stopped reading.")] + public static partial void PushTimedOut(ILogger logger, string symbol, long sequence, double timeoutMilliseconds); + + [LoggerMessage(EventId = 1200, Level = LogLevel.Information, Message = "WebSocket client subscribed to {Symbol}.")] + public static partial void WebSocketSubscribed(ILogger logger, string symbol); + + [LoggerMessage(EventId = 1201, Level = LogLevel.Information, Message = "WebSocket client for {Symbol} dropped: {Reason}.")] + public static partial void WebSocketDropped(ILogger logger, string symbol, string reason); + + [LoggerMessage(EventId = 1202, Level = LogLevel.Information, Message = "WebSocket client for {Symbol} disconnected.")] + public static partial void WebSocketDisconnected(ILogger logger, string symbol); +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Hubs/IndicatorHub.cs b/samples/TechnicalAnalysis.Samples.RealTime/Hubs/IndicatorHub.cs new file mode 100644 index 00000000..8ea5b00e --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Hubs/IndicatorHub.cs @@ -0,0 +1,186 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Runtime.CompilerServices; +using System.Threading.Channels; +using Microsoft.AspNetCore.SignalR; +using TechnicalAnalysis.Samples.RealTime.Contracts; +using TechnicalAnalysis.Samples.RealTime.Streaming; + +namespace TechnicalAnalysis.Samples.RealTime.Hubs; + +/// +/// The SignalR surface of the sample, offering the same data two ways. +/// +/// +/// +/// Push β€” call and handle the snapshot client method. +/// The connection joins a per-symbol group and the server pushes each closed bar to the whole group. One +/// send per group per bar regardless of how many clients are watching, which is what you want when +/// thousands of browsers follow the same instrument. +/// +/// +/// Streaming β€” call and consume the +/// . Each caller gets its own bounded queue, its own cancellation, and +/// disposal that is guaranteed to run when the client walks away or the connection dies. Prefer this when +/// a caller wants per-request lifetime rather than a connection-wide subscription. +/// +/// +/// Both paths hand out the same instances and drop under the same +/// backpressure policy; see . +/// +/// +/// The snapshot distribution point every transport reads from. +public sealed class IndicatorHub(SnapshotBroadcaster broadcaster) : Hub +{ + /// The client method name the server invokes with each closed bar's snapshot. + public const string SnapshotMethod = "snapshot"; + + private readonly SnapshotBroadcaster _broadcaster = broadcaster; + + /// + /// Builds the SignalR group name used for a symbol. + /// + /// The resolved symbol. + /// The group name. + public static string GroupFor(string symbol) => $"symbol:{symbol}"; + + /// + /// Lists the symbols this server publishes. + /// + /// The configured symbols, in configuration order. + [HubMethodName("GetSymbols")] + public IReadOnlyList GetSymbols() => _broadcaster.Symbols; + + /// + /// Subscribes this connection to a symbol's push stream and immediately replays the latest snapshot. + /// + /// The symbol to follow. Case insensitive. + /// A task that completes once the connection has joined the group. + /// The symbol is not published by this server. + [HubMethodName("Subscribe")] + public async Task SubscribeAsync(string symbol) + { + string resolved = Resolve(symbol); + + await Groups.AddToGroupAsync(Context.ConnectionId, GroupFor(resolved), Context.ConnectionAborted); + _broadcaster.TrackGroupJoin(Context.ConnectionId, resolved); + + // Replaying the last bar means a client that connects mid-period sees data now rather than after + // the next boundary. The replayed snapshot carries its own sequence number, so a client that + // reconnects can tell whether it already had this bar. + if (_broadcaster.Latest(resolved) is { } latest) + { + await Clients.Caller.SendAsync(SnapshotMethod, latest, Context.ConnectionAborted); + } + } + + /// + /// Unsubscribes this connection from a symbol's push stream. + /// + /// The symbol to stop following. Case insensitive. + /// A task that completes once the connection has left the group. + /// The symbol is not published by this server. + [HubMethodName("Unsubscribe")] + public async Task UnsubscribeAsync(string symbol) + { + string resolved = Resolve(symbol); + + await Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupFor(resolved), Context.ConnectionAborted); + _broadcaster.TrackGroupLeave(Context.ConnectionId, resolved); + } + + /// + public override Task OnDisconnectedAsync(Exception? exception) + { + // SignalR drops a dead connection from its groups without telling us which ones they were, so the + // membership counter has to be reconciled here or /health would report a leak that does not exist. + _broadcaster.TrackConnectionClosed(Context.ConnectionId); + return base.OnDisconnectedAsync(exception); + } + + /// + /// Streams a symbol's snapshots to the caller until the caller cancels or the connection ends. + /// + /// The symbol to follow. Case insensitive. + /// + /// Supplied by SignalR. It fires when the client disposes its stream, when the connection drops, and + /// when the host shuts down; honouring it is what makes the subscription leak free. + /// + /// An async sequence of snapshots, newest last. + /// The symbol is not published by this server. + [HubMethodName("StreamSnapshots")] + public async IAsyncEnumerable StreamSnapshotsAsync( + string symbol, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + string resolved = Resolve(symbol); + + // Subscribe before yielding anything so no bar can slip between the replay and the live stream. + using FanoutSubscription subscription = _broadcaster.Subscribe(resolved); + + // Subscribing before the replay closes the loss window and opens a duplication one: a publish that + // interleaves between the broadcaster storing "latest" and fanning out delivers the same bar twice. + // Tracking the highest sequence yielded closes that too, so the stream is strictly increasing. + long lastSequence = -1; + + if (_broadcaster.Latest(resolved) is { } latest) + { + lastSequence = latest.Sequence; + yield return latest; + } + + while (true) + { + IndicatorSnapshot? next = await ReadNextAsync(subscription.Reader, cancellationToken); + if (next is null) + { + yield break; + } + + if (next.Sequence <= lastSequence) + { + continue; + } + + lastSequence = next.Sequence; + yield return next; + } + } + + /// + /// Reads the next snapshot, translating the two normal end-of-stream conditions into a null so the + /// iterator can finish without an exception. An iterator cannot yield from inside a try/catch, which + /// is why this lives in its own method. + /// + private static async Task ReadNextAsync( + ChannelReader reader, + CancellationToken cancellationToken) + { + try + { + return await reader.ReadAsync(cancellationToken); + } + catch (OperationCanceledException) + { + return null; + } + catch (ChannelClosedException) + { + return null; + } + } + + private string Resolve(string symbol) + { + if (!_broadcaster.TryResolveSymbol(symbol, out string resolved)) + { + throw new HubException($"Unknown symbol '{symbol}'. Call GetSymbols for the list this server publishes."); + } + + return resolved; + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Indicators/RollingIndicatorEngine.cs b/samples/TechnicalAnalysis.Samples.RealTime/Indicators/RollingIndicatorEngine.cs new file mode 100644 index 00000000..0a43832a --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Indicators/RollingIndicatorEngine.cs @@ -0,0 +1,354 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using TechnicalAnalysis.Common; +using TechnicalAnalysis.Functions; +using TechnicalAnalysis.Samples.RealTime.Configuration; +using TechnicalAnalysis.Samples.RealTime.Contracts; + +namespace TechnicalAnalysis.Samples.RealTime.Indicators; + +/// +/// Keeps the last N closed bars for one symbol and, on every new bar, recomputes each indicator over that +/// window and publishes the latest value of each. +/// +/// +/// Why recompute a window instead of updating incrementally +/// +/// TA-Lib is a batch API: every function takes a whole array and returns a whole array. Streaming code has +/// two ways to live with that. +/// +/// +/// The first is a true incremental update: keep each indicator's internal state β€” a running sum for SMA, +/// the previous value and smoothing factor for EMA, Wilder's averages for RSI and ATR β€” and advance it by +/// one bar. That is O(1) per bar and per indicator, and it is what a production feed handler does. It also +/// means reimplementing, and then keeping in step with, the exact arithmetic of every indicator you use. +/// The moment your EMA seeding or your RSI smoothing differs from TA-Lib's by an epsilon, your live values +/// and your backtest values quietly disagree, and that difference is very hard to find later. +/// +/// +/// The second, used here, is to recompute over a fixed trailing window. It costs O(window) per indicator +/// per bar, which for a 256-bar window and seven indicators is a few microseconds β€” nothing next to one +/// bar per second, and nothing next to the network hop that follows. In exchange, the number a client sees +/// is produced by the same library, on the same code path, as the number a backtest sees. For a sample +/// whose entire point is showing TaLibStandard used correctly, that equivalence is worth far more than +/// the saved cycles. Reach for incremental state when the bar rate or the symbol count makes the window +/// recompute show up in a profile, and then pin it with tests against these values. +/// +/// Window size versus the largest lookback +/// +/// The window must be strictly larger than the slowest indicator's lookback, or that indicator never +/// produces a value. With the defaults the lookbacks are: SMA(30) needs 30 bars, EMA(20) 20, RSI(14) 15, +/// Bollinger(20) 20, ATR(14) 15, and MACD(12,26,9) 34 β€” the slow EMA's 26 bars plus the signal EMA's 9, +/// less one. So 34 bars is the floor, exposed as . +/// +/// +/// The default window is 256, roughly seven times that floor, and the margin is deliberate. Indicators +/// with exponential smoothing have no exact finite lookback: TA-Lib's EMA seeds itself with a simple +/// average of the first period and then decays, so an EMA computed over the last 34 bars is not the same +/// number as an EMA computed over the last 5000 β€” the difference is the seed's residual weight, which +/// decays by a factor of (1 - 2/(n+1)) per bar. At 256 bars an EMA(20) retains about +/// (1 - 2/21)^236 β‰ˆ 4e-11 of its seed, which is far below display precision. A window of 40 would not be: +/// it would produce plausible-looking values that drift from the batch answer. Choose the window from the +/// slowest smoothed indicator's decay, not from its nominal period. +/// +/// This type is not thread safe; each symbol gets its own instance driven by the pipeline loop. +/// +public sealed class RollingIndicatorEngine +{ + private readonly string _symbol; + private readonly IndicatorOptions _settings; + private readonly int _capacity; + + // Ring storage. _next is where the following bar is written; _count saturates at _capacity. + private readonly double[] _ringHigh; + private readonly double[] _ringLow; + private readonly double[] _ringClose; + private int _next; + private int _count; + + // Chronological scratch buffers handed to TA-Lib. They stay at capacity length for the life of the + // engine, so the steady state allocates nothing here; TA-Lib only ever reads indices [0, endIdx]. + private readonly double[] _windowHigh; + private readonly double[] _windowLow; + private readonly double[] _windowClose; + + private long _sequence; + + /// + /// Initializes a new instance of the class. + /// + /// The symbol this engine tracks. + /// How many closed bars to keep. + /// The indicator periods to use. + /// + /// The window is not larger than the slowest indicator's lookback. + /// + public RollingIndicatorEngine(string symbol, int windowSize, IndicatorOptions settings) + { + ArgumentException.ThrowIfNullOrWhiteSpace(symbol); + ArgumentNullException.ThrowIfNull(settings); + + _symbol = symbol; + _settings = settings; + _capacity = windowSize; + BarsRequired = LargestLookback(settings) + 1; + + ArgumentOutOfRangeException.ThrowIfLessThan(windowSize, BarsRequired); + + _ringHigh = new double[windowSize]; + _ringLow = new double[windowSize]; + _ringClose = new double[windowSize]; + _windowHigh = new double[windowSize]; + _windowLow = new double[windowSize]; + _windowClose = new double[windowSize]; + } + + /// + /// Gets the number of bars the slowest configured indicator needs before it yields its first value. + /// + public int BarsRequired { get; } + + /// + /// Computes the number of bars of history the slowest configured indicator consumes before producing + /// a value, expressed as a TA-Lib lookback (values start at input index lookback). + /// + /// The indicator periods. + /// The largest lookback across the configured indicators. + public static int LargestLookback(IndicatorOptions settings) + { + ArgumentNullException.ThrowIfNull(settings); + + // TA-Lib swaps the two MACD periods internally when the "fast" one is the longer of the pair, so the + // lookback is driven by the larger period, not by whichever field is named Slow. Reading the field + // name instead would under-report BarsRequired for a swapped configuration and let the startup + // window check pass with a window too small for MACD ever to print. + int macdSlow = Math.Max(settings.MacdFastPeriod, settings.MacdSlowPeriod); + + // These mirror TA-Lib's own lookback functions: a moving average consumes period - 1 bars, while + // Wilder-smoothed RSI and ATR consume a full period, and MACD stacks the slow EMA's lookback on + // top of the signal EMA's. + int[] lookbacks = + [ + settings.SmaFastPeriod - 1, + settings.SmaSlowPeriod - 1, + settings.EmaPeriod - 1, + settings.RsiPeriod, + macdSlow - 1 + (settings.MacdSignalPeriod - 1), + settings.BollingerPeriod - 1, + settings.AtrPeriod + ]; + + return lookbacks.Max(); + } + + /// + /// Adds a closed bar and recomputes every indicator over the resulting window. + /// + /// The bar that just closed. + /// The snapshot describing that bar. + public IndicatorSnapshot Update(Bar bar) + { + ArgumentNullException.ThrowIfNull(bar); + + Append(bar); + Materialize(); + + // The window is a plain chronological array now, so the newest bar sits at index _count - 1 and + // every TA-Lib call below runs over [0, lastIndex]. + int lastIndex = _count - 1; + _sequence++; + + double? smaFast = Latest(TAMath.Sma(0, lastIndex, _windowClose, _settings.SmaFastPeriod), lastIndex); + double? smaSlow = Latest(TAMath.Sma(0, lastIndex, _windowClose, _settings.SmaSlowPeriod), lastIndex); + double? ema = Latest(TAMath.Ema(0, lastIndex, _windowClose, _settings.EmaPeriod), lastIndex); + double? rsi = Latest(TAMath.Rsi(0, lastIndex, _windowClose, _settings.RsiPeriod), lastIndex); + double? atr = Latest( + TAMath.Atr(0, lastIndex, _windowHigh, _windowLow, _windowClose, _settings.AtrPeriod), + lastIndex); + + MacdResult macd = TAMath.Macd( + 0, + lastIndex, + _windowClose, + _settings.MacdFastPeriod, + _settings.MacdSlowPeriod, + _settings.MacdSignalPeriod); + + double? macdValue = Latest(macd, macd.MacdValue, lastIndex); + double? macdSignal = Latest(macd, macd.MacdSignal, lastIndex); + double? macdHistogram = Latest(macd, macd.MacdHist, lastIndex); + + BollingerBandsResult bands = TAMath.BollingerBands( + 0, + lastIndex, + _windowClose, + _settings.BollingerPeriod, + _settings.BollingerDeviations, + _settings.BollingerDeviations); + + double? upper = Latest(bands, bands.RealUpperBand, lastIndex); + double? middle = Latest(bands, bands.RealMiddleBand, lastIndex); + double? lower = Latest(bands, bands.RealLowerBand, lastIndex); + + Signal signal = Classify(rsi, smaFast, smaSlow, macdHistogram); + + return new IndicatorSnapshot( + _symbol, + bar.Timestamp, + bar.Close, + _sequence, + smaFast, + smaSlow, + ema, + rsi, + macdValue, + macdSignal, + macdHistogram, + upper, + middle, + lower, + atr, + signal, + _count, + BarsRequired); + } + + /// + /// Extracts the value a single-output TA-Lib result holds for the newest input bar. + /// + /// The result returned by TAMath. + /// The index, in the input array, of the newest bar. + /// The newest value, or if the indicator has not warmed up. + private static double? Latest(SingleOutputResult result, int lastInputIndex) + => Latest(result, result.Real, lastInputIndex); + + /// + /// Extracts the value one output array of a TA-Lib result holds for the newest input bar. + /// + /// + /// This is the alignment rule, and getting it wrong shifts every signal in time. + /// + /// TAMath allocates an output array of endIdx - startIdx + 1 elements and fills it from index + /// zero. The output is not parallel to the input. Output element k describes input index + /// BegIdx + k, for k in [0, NBElement); every element from NBElement + /// onwards is an uninitialised zero that means nothing. + /// + /// + /// So the newest value is output[NBElement - 1] β€” never output[lastInputIndex], which + /// with a 256-bar window and a 30-bar lookback would read 29 elements past the end of the real data + /// and hand back a zero dressed up as a price. The mapping is asserted rather than assumed: the value + /// is returned only when BegIdx + NBElement - 1 really is the newest bar's index. + /// + /// + /// The result returned by TAMath. + /// The specific output array to read. + /// The index, in the input array, of the newest bar. + /// The newest value, or if the indicator has not warmed up. + private static double? Latest(IndicatorResult result, double[] output, int lastInputIndex) + { + if (result.RetCode != RetCode.Success || result.NBElement <= 0) + { + return null; + } + + int newestOutputIndex = result.NBElement - 1; + int newestInputIndex = result.BegIdx + newestOutputIndex; + + if (newestInputIndex != lastInputIndex || newestOutputIndex >= output.Length) + { + // The indicator did not cover the newest bar. Returning the previous bar's value here is the + // classic streaming bug: it looks like data and is silently one bar stale. + return null; + } + + double value = output[newestOutputIndex]; + return double.IsFinite(value) ? value : null; + } + + /// + /// Derives the sample's combined read. Every input is optional, and any missing input degrades the + /// answer to rather than guessing. + /// + private Signal Classify(double? rsi, double? smaFast, double? smaSlow, double? macdHistogram) + { + if (rsi is { } rsiValue) + { + if (rsiValue >= _settings.RsiOverbought) + { + return Signal.Overbought; + } + + if (rsiValue <= _settings.RsiOversold) + { + return Signal.Oversold; + } + } + + if (smaFast is not { } fast || smaSlow is not { } slow || macdHistogram is not { } histogram) + { + return Signal.Neutral; + } + + if (fast > slow && histogram > 0.0) + { + return Signal.Bullish; + } + + if (fast < slow && histogram < 0.0) + { + return Signal.Bearish; + } + + return Signal.Neutral; + } + + /// + /// Writes a bar into the ring, overwriting the oldest once the window is full. + /// + private void Append(Bar bar) + { + _ringHigh[_next] = (double)bar.High; + _ringLow[_next] = (double)bar.Low; + _ringClose[_next] = (double)bar.Close; + + _next = (_next + 1) % _capacity; + if (_count < _capacity) + { + _count++; + } + } + + /// + /// Copies the ring into the scratch buffers in chronological order, oldest first. + /// + /// + /// TA-Lib needs contiguous ascending input, so the ring is unrolled with at most two block copies: + /// the tail of the ring, then its head. Both are Array.Copy, which is a memmove. + /// + private void Materialize() + { + if (_count < _capacity) + { + // Not wrapped yet: the ring is already chronological from index 0. + Array.Copy(_ringHigh, _windowHigh, _count); + Array.Copy(_ringLow, _windowLow, _count); + Array.Copy(_ringClose, _windowClose, _count); + return; + } + + int oldest = _next; + int tail = _capacity - oldest; + + Array.Copy(_ringHigh, oldest, _windowHigh, 0, tail); + Array.Copy(_ringLow, oldest, _windowLow, 0, tail); + Array.Copy(_ringClose, oldest, _windowClose, 0, tail); + + Array.Copy(_ringHigh, 0, _windowHigh, tail, oldest); + Array.Copy(_ringLow, 0, _windowLow, tail, oldest); + Array.Copy(_ringClose, 0, _windowClose, tail, oldest); + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Program.cs b/samples/TechnicalAnalysis.Samples.RealTime/Program.cs new file mode 100644 index 00000000..6fdd129b --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Program.cs @@ -0,0 +1,148 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Net.WebSockets; +using TechnicalAnalysis.Samples.RealTime.Configuration; +using TechnicalAnalysis.Samples.RealTime.Contracts; +using TechnicalAnalysis.Samples.RealTime.Hubs; +using TechnicalAnalysis.Samples.RealTime.Indicators; +using TechnicalAnalysis.Samples.RealTime.Streaming; +using TechnicalAnalysis.Samples.RealTime.WebSockets; + +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); + +// --------------------------------------------------------------------------- +// Options. Everything the sample can be tuned with lives in the RealTime +// section and can be overridden on the command line, for example: +// dotnet run -- --RealTime:BarSeconds=1 --RealTime:TickIntervalMilliseconds=100 +// --------------------------------------------------------------------------- +builder.Services + .AddOptions() + .Bind(builder.Configuration.GetSection(RealTimeOptions.SectionName)) + .Validate(RealTimeOptionsValidation.IsValid, RealTimeOptionsValidation.Message) + .ValidateOnStart(); + +// --------------------------------------------------------------------------- +// Serialization. The source generated context is wired into all three exits: +// minimal API responses, the SignalR JSON protocol, and the raw socket handler +// (which references RealTimeJsonContext.Default directly). +// --------------------------------------------------------------------------- +builder.Services.ConfigureHttpJsonOptions(options => + options.SerializerOptions.TypeInfoResolverChain.Insert(0, RealTimeJsonContext.Default)); + +builder.Services + .AddSignalR() + .AddJsonProtocol(options => + options.PayloadSerializerOptions.TypeInfoResolverChain.Insert(0, RealTimeJsonContext.Default)); + +// --------------------------------------------------------------------------- +// The pipeline. The feed is registered twice on purpose: once as the singleton +// everything subscribes to, once as the hosted service that drives it. +// --------------------------------------------------------------------------- +builder.Services.AddSingleton(); +builder.Services.AddHostedService(provider => provider.GetRequiredService()); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddHostedService(provider => provider.GetRequiredService()); + +WebApplication app = builder.Build(); + +app.UseWebSockets(); +app.UseDefaultFiles(); +app.UseStaticFiles(); + +// SignalR transport: push via per-symbol groups, plus a server-to-client streaming method. +app.MapHub("/hubs/indicators"); + +// Raw WebSocket transport, for clients that would rather not take a SignalR dependency. +app.Map("/ws/indicators", async (HttpContext context, IndicatorWebSocketHandler handler) => +{ + if (!context.WebSockets.IsWebSocketRequest) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + await context.Response.WriteAsync( + "This endpoint speaks WebSocket. Open / in a browser for the live dashboard, " + + "or connect a client to ws:///ws/indicators?symbol=."); + return; + } + + string? symbol = context.Request.Query["symbol"]; + using WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(); + await handler.HandleAsync(socket, symbol, context.RequestAborted); +}); + +DateTimeOffset startedAt = DateTimeOffset.UtcNow; + +app.MapGet("/health", (SyntheticMarketDataFeed feed, SnapshotBroadcaster broadcaster, IndicatorPipeline pipeline) + => new HealthResponse( + "healthy", + DateTimeOffset.UtcNow, + Math.Round((DateTimeOffset.UtcNow - startedAt).TotalSeconds, 1), + feed.Symbols, + feed.PublishedTicks, + pipeline.BarsClosed, + feed.SubscriberCount, + broadcaster.ChannelSubscriberCount, + broadcaster.GroupSubscriberCount, + pipeline.PushesDropped)); + +app.MapGet("/api/symbols", (SnapshotBroadcaster broadcaster) => broadcaster.Symbols); + +await app.RunAsync(); + +/// +/// Startup validation for . Catching a bad period here turns a per-bar +/// RetCode.BadParam β€” which would silently null an indicator forever β€” into a loud failure at boot. +/// +internal static class RealTimeOptionsValidation +{ + /// The message shown when validation fails. + public const string Message = + "RealTime options are invalid: need at least one symbol, a positive tick interval, bar period and " + + "queue capacity, indicator periods of at least 2, and a window size larger than the slowest " + + "indicator's lookback."; + + /// + /// Checks that the bound options can actually drive the pipeline. + /// + /// The bound options. + /// when the options are usable. + public static bool IsValid(RealTimeOptions options) + { + if (options.Symbols.Length == 0 || options.Symbols.Any(string.IsNullOrWhiteSpace)) + { + return false; + } + + if (options.TickIntervalMilliseconds <= 0 || options.BarSeconds <= 0 || options.SubscriberQueueCapacity <= 0) + { + return false; + } + + IndicatorOptions indicators = options.Indicators; + int[] periods = + [ + indicators.SmaFastPeriod, + indicators.SmaSlowPeriod, + indicators.EmaPeriod, + indicators.RsiPeriod, + indicators.MacdFastPeriod, + indicators.MacdSlowPeriod, + indicators.MacdSignalPeriod, + indicators.BollingerPeriod, + indicators.AtrPeriod + ]; + + // TAMath rejects any period below 2 with RetCode.BadParam. + if (periods.Any(period => period < 2)) + { + return false; + } + + return options.WindowSize > RollingIndicatorEngine.LargestLookback(indicators); + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/README.md b/samples/TechnicalAnalysis.Samples.RealTime/README.md new file mode 100644 index 00000000..19986f15 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/README.md @@ -0,0 +1,223 @@ +# Real-time indicators over WebSocket and SignalR + +A streaming server that turns a synthetic tick feed into OHLCV bars, recomputes seven TaLibStandard +indicators β€” SMA fast, SMA slow, EMA, RSI, ATR, MACD and Bollinger Bands, eleven output series in all β€” +on every closed bar, and pushes the results to browsers and .NET clients over **both** +transports the issue asks for: raw WebSocket and SignalR. + +Everything runs offline. There is no market data provider, no API key, and no network call β€” the price +path comes from a seeded random walk, so the same seed always produces the same prices. + +``` +ticks ──► BarAggregator ──► RollingIndicatorEngine ──► IndicatorSnapshot ──┬──► SignalR /hubs/indicators + (feed) 1s..Ns bars window recompute nullable values └──► WebSocket /ws/indicators +``` + +## Run it + +```bash +# server, with the default 5-second bars +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime + +# then open http://localhost:5199 +``` + +The port comes from `"Urls"` in `appsettings.json` rather than a launch profile, because +`Properties/launchSettings.json` is gitignored repo-wide and would not survive a clone. Override it with +`--urls http://127.0.0.1:8080` like any other host setting. + +Five-second bars take about three minutes before every indicator is warm. For a quicker look, shorten +the bar period: + +```bash +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime -- \ + --urls http://127.0.0.1:5199 \ + --RealTime:BarSeconds=1 \ + --RealTime:TickIntervalMilliseconds=100 +``` + +The console client lives in the sibling project: + +```bash +# push transport (SignalR groups), default symbol +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime.Client + +# a specific symbol, the hub's streaming method, and a time limit +dotnet run --project samples/TechnicalAnalysis.Samples.RealTime.Client -- \ + http://127.0.0.1:5199 --symbol GLOBEX --stream --seconds 30 +``` + +## Endpoints + +| Endpoint | What it does | +| --- | --- | +| `GET /` | The live dashboard. One file, no CDN, no npm, no build step. | +| `GET /health` | Uptime, symbols, ticks published, bars closed, `snapshotSubscribers` (channel subscribers: raw WebSocket and hub streaming), `groupSubscribers` (SignalR push subscriptions) and `pushesDropped`. | +| `GET /api/symbols` | The symbols this server publishes. | +| `WS /ws/indicators?symbol=ACME` | Raw socket. Omit `symbol` for the first configured one. | +| `/hubs/indicators` | SignalR hub. | + +### SignalR hub methods + +| Method | Kind | Notes | +| --- | --- | --- | +| `GetSymbols()` | invoke | Returns the symbol list. | +| `Subscribe(symbol)` | invoke | Joins the symbol's group; the latest snapshot is replayed immediately. | +| `Unsubscribe(symbol)` | invoke | Leaves the group. | +| `StreamSnapshots(symbol)` | stream | `IAsyncEnumerable`, honours the caller's cancellation token. | +| `snapshot` | client method | Invoked by the server with each closed bar's snapshot. | + +Group membership is per connection, so a client that reconnects must call `Subscribe` again β€” the +console client shows how. + +### Raw socket frames + +One `session` frame on connect, then one `snapshot` frame per closed bar. Real frames captured from a +running server, wrapped for width and with the always-null sibling properties elided: + +```json +{"type":"session","session":{"symbol":"ACME","symbols":["ACME","GLOBEX","INITECH"],"barSeconds":1, + "tickIntervalMilliseconds":100,"windowSize":256,"periods":{"smaFast":10,"smaSlow":30,"ema":20,"rsi":14, + "macdFast":12,"macdSlow":26,"macdSignal":9,"bollinger":20,"bollingerDeviations":2,"atr":14}}} + +{"type":"snapshot","snapshot":{"symbol":"ACME","timestamp":"2026-07-27T12:18:36+00:00","close":343.65, + "sequence":308,"smaFast":343.964,"smaSlow":347.01933333333307,"ema":345.0055987766077, + "rsi":43.62551333468498,"macd":-1.3233998227737516,"macdSignal":-1.127486559231894, + "macdHistogram":-0.19591326354185767,"bollingerUpper":348.25611962208296, + "bollingerMiddle":344.9424999999999,"bollingerLower":341.6288803779168,"atr":4.706082652336289E+267, + "signal":"Bearish","barsInWindow":256,"barsRequired":34,"isWarmedUp":true}} +``` + +That `atr` is not a typo β€” see [Known library defects](#known-library-defects-that-show-up-here). + +An unknown symbol gets an `error` frame followed by a `PolicyViolation` close. + +## The part worth reading: output alignment + +`RollingIndicatorEngine` is where a streaming integration usually goes wrong, and the whole sample +exists to get it right. + +TA-Lib is a batch API. `TAMath.Sma(0, lastIndex, close, 30)` allocates an output array of +`lastIndex + 1` elements and fills it **from index zero** β€” the output is not parallel to the input. +The mapping is: + +``` +output[k] describes input index (BegIdx + k), for k in [0, NBElement) +``` + +Everything from `NBElement` onwards is an uninitialised zero that means nothing. So the newest value is +`output[NBElement - 1]`, never `output[lastIndex]`. Reading `output[lastIndex]` with a 256-bar window and +a 30-bar lookback lands 29 elements past the real data and returns a zero dressed up as a price β€” and +because zero is a plausible-looking number, nothing downstream notices. + +The engine asserts the mapping instead of assuming it: it computes `BegIdx + NBElement - 1` and returns +the value only if that really is the newest bar's index. Otherwise it returns `null`. + +**Null means null.** An indicator that has not warmed up is reported as `null`, never as `0` and never as +the previous bar's value. Both of those substitutions are indistinguishable from a real reading once the +number leaves the process. The dashboard renders a null as a dashed rule with a warm-up hairline, so you +can watch each indicator switch on. + +## Window recompute versus incremental state + +The engine keeps a fixed-size ring buffer of the last N closed bars and recomputes every indicator over +that window on each new bar. The alternative β€” keeping each indicator's internal state and advancing it +by one bar β€” is O(1) instead of O(window), and it is what a production feed handler does at scale. It +also means reimplementing the exact arithmetic of every indicator you use and keeping it in step with the +library forever; the day your EMA seeding differs by an epsilon, your live values and your backtest values +disagree and the difference is very hard to find. + +Recomputing costs a few microseconds per bar and buys you the guarantee that the number a client sees +came from the same library, on the same code path, as the number a backtest sees. + +**Window size.** With the defaults the largest lookback is MACD(12,26,9) at 34 bars, exposed as +`barsRequired`. The window defaults to 256, roughly seven times that, and the margin is deliberate: +exponentially smoothed indicators have no exact finite lookback. TA-Lib seeds an EMA with a simple +average and then decays it, so the seed's residual weight falls by `(1 - 2/(n+1))` per bar. At 256 bars an +EMA(20) retains about 4e-11 of its seed β€” invisible. A 40-bar window would produce plausible values that +quietly drift from the batch answer. Size the window from the slowest smoothed indicator's decay, not +from its nominal period. + +## Backpressure + +Every subscriber β€” each browser tab, each console client, each hub stream β€” gets its own bounded channel +created with `BoundedChannelFullMode.DropOldest`. A consumer that stops reading loses its oldest queued +frames; it never stalls the producer. + +That is the right policy for a market feed and the wrong one for an order feed. A price that is 40 bars +late is worth nothing: a slow consumer wants the current bar, not a replay of the last forty. `Wait` +would let the slowest subscriber throttle every other subscriber, which is how one wedged client takes +down a whole feed; an unbounded channel trades that stall for unbounded memory. Dropping is not silent β€” +`sequence` increments by one per bar, so a client can see the gap, and the console client counts them. + +Anything that must not be lost belongs on a different, acknowledged channel. + +## Configuration + +All of it binds from the `RealTime` section of `appsettings.json` and can be overridden on the command +line (`--RealTime:BarSeconds=1`) or by environment variable (`RealTime__BarSeconds=1`). + +| Key | Default | Meaning | +| --- | --- | --- | +| `Symbols` | `ACME`, `GLOBEX`, `INITECH` | Instruments to publish. The first is the page's default. | +| `TickIntervalMilliseconds` | `250` | Time between synthetic ticks. | +| `BarSeconds` | `5` | Bar period. Bars align to absolute time, not to server start. | +| `RandomSeed` | `20240613` | Seeds the price walk. Same seed, same prices. | +| `WindowSize` | `256` | Bars kept per symbol. Must exceed the largest lookback. | +| `SubscriberQueueCapacity` | `64` | Frames a subscriber may buffer before the oldest is dropped. | +| `Volatility` | `0.0015` | Per-tick log-return standard deviation. | +| `MeanReversion` | `0.0025` | Pull back towards each symbol's base price, per tick. | +| `Indicators:*` | see file | Periods for SMA fast/slow, EMA, RSI, MACD, Bollinger, ATR. | + +Options are validated at startup with `ValidateOnStart`, so a period below 2 β€” which `TAMath` rejects +with `RetCode.BadParam` and which would therefore null an indicator forever β€” fails the boot instead. + +`Symbols` deliberately has no code-side default. The configuration binder *appends* to a collection that +already holds items, so three defaults plus three in `appsettings.json` bind to six duplicated symbols. +Startup validation rejects an empty list, which turns a missing configuration file into a clear error. + +## Determinism + +The seed fixes the price path exactly. It does not fix bar contents: tick timestamps come from the real +clock, so which ticks land in which bar depends on timer jitter and machine load. Drive the aggregator +from a virtual clock if you need bars reproducible to the cent. + +## Known library defects that show up here + +Two bugs in `src/TechnicalAnalysis.Functions` are visible in this sample's output. They are library +issues, not sample issues, and the sample deliberately calls the library the recommended way rather than +working around them. The same list, with the full blast radius, is in +[Known library defects](../../docs/guides/getting-started.md#10-known-library-defects). + +1. **`ATR` diverges.** `Atr/TAFunc.cs` multiplies the running average by `period - 1` and adds the new + true range, but only divides the *stored* value by `period` β€” the accumulator itself is never + normalised, so it grows by a factor of `period - 1` every bar. On a constant series with true range + 2.0 and `period` 14, ATR reads `2, 2, 26.14, 340, 4420, …` and reaches 1e27 within 40 bars. Only the + first two output elements are correct. Until it is fixed, ignore the ATR column. + +2. **`EMA` seeds itself low.** `TAFunc.cs`'s `TA_INT_EMA` seed loop sums `period - 1` values and then + divides by `period`, so the seed is the true average scaled by `(period - 1) / period`. On a constant + series of 100, EMA(20) starts at 95.48 instead of 100. The error decays with the smoothing factor, so + a long window hides it in steady state β€” but it distorts every value for the first few dozen bars, and + it propagates to MACD, DEMA, TEMA, T3, APO, PPO and TRIX. MACD over a constant series returns 0.53 + where it should return 0. + +A third, milder one: `Rsi/TAFunc.cs` divides by `prevGain + prevLoss` without the zero guard TA-Lib has, +so a perfectly flat series yields `NaN` rather than `0`. The engine's `double.IsFinite` check converts +that to `null`, which is why the dashboard shows a dash rather than `NaN`. + +## Files + +| Path | Role | +| --- | --- | +| `Contracts/` | Wire types plus the `JsonSerializerContext` used by all three exits. | +| `Configuration/RealTimeOptions.cs` | Everything tunable, bound via `IOptions`. | +| `Streaming/SyntheticMarketDataFeed.cs` | Seeded tick generator, `BackgroundService`, fans out to N subscribers. | +| `Streaming/BarAggregator.cs` | Ticks to OHLCV bars on absolute time boundaries. | +| `Streaming/BoundedFanout.cs` | The drop-oldest fanout and its race-free subscription handle. | +| `Streaming/SnapshotBroadcaster.cs` | Per-symbol distribution plus last-snapshot replay. | +| `Streaming/IndicatorPipeline.cs` | The single loop: tick in, bar out, snapshot out, both transports. The SignalR group send runs on its own bounded pump so a wedged subscriber cannot stall the loop. | +| `Indicators/RollingIndicatorEngine.cs` | Ring buffer, window recompute, alignment, warm-up nulls. | +| `Hubs/IndicatorHub.cs` | SignalR push and streaming. | +| `WebSockets/IndicatorWebSocketHandler.cs` | Raw socket, close handshake, cancellation. | +| `wwwroot/index.html` | The dashboard. Vanilla JS and canvas, zero dependencies. | diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Streaming/BarAggregator.cs b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/BarAggregator.cs new file mode 100644 index 00000000..d2808f43 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/BarAggregator.cs @@ -0,0 +1,171 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using TechnicalAnalysis.Samples.RealTime.Contracts; + +namespace TechnicalAnalysis.Samples.RealTime.Streaming; + +/// +/// Folds a tick stream for one symbol into fixed-period OHLCV bars. +/// +/// +/// +/// Bars are aligned to absolute time, not to the first tick seen: a tick's bucket is its Unix timestamp +/// floored to the bar period. Two servers started seconds apart therefore agree on bar boundaries, and a +/// client can label a bar without asking when the server booted. +/// +/// +/// A bar is closed by the arrival of the first tick belonging to a later bucket. That is the standard +/// aggregator contract and it has one consequence worth stating: a period with no ticks in it produces no +/// bar, and the previous bar stays open until trading resumes. With the synthetic feed ticking several +/// times a second that never happens; against a real venue at 3am it happens constantly, and downstream +/// code must treat bar timestamps as a sparse series rather than a metronome. +/// +/// +/// This type is not thread safe. Each symbol gets its own instance, driven by the single pipeline loop. +/// +/// +public sealed class BarAggregator +{ + private readonly string _symbol; + private readonly long _periodMilliseconds; + + private long _bucketStart = -1; + private decimal _open; + private decimal _high; + private decimal _low; + private decimal _close; + private long _volume; + private int _tickCount; + + /// + /// Initializes a new instance of the class. + /// + /// The symbol whose ticks this aggregator folds. + /// The bar period. Must be positive. + /// The period is not positive. + public BarAggregator(string symbol, TimeSpan period) + { + ArgumentException.ThrowIfNullOrWhiteSpace(symbol); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(period, TimeSpan.Zero); + + _symbol = symbol; + _periodMilliseconds = (long)period.TotalMilliseconds; + } + + /// + /// Gets a value indicating whether a bar is currently open. + /// + public bool HasOpenBar => _bucketStart >= 0; + + /// + /// Adds a tick. + /// + /// The tick to fold in. + /// + /// The bar that just closed, if this tick crossed a period boundary; otherwise . + /// + public Bar? Add(Tick tick) + { + ArgumentNullException.ThrowIfNull(tick); + + long bucket = FloorToBucket(tick.Timestamp); + + if (_bucketStart < 0) + { + StartBucket(bucket, tick); + return null; + } + + if (bucket == _bucketStart) + { + Accumulate(tick); + return null; + } + + // A late tick belonging to an already-closed bucket is dropped rather than back-filled: rewriting + // a bar a client has already charted is worse than ignoring one print. + if (bucket < _bucketStart) + { + return null; + } + + Bar closed = BuildBar(); + StartBucket(bucket, tick); + return closed; + } + + /// + /// Closes the in-progress bar, if any. Called on shutdown so the last partial period is not lost. + /// + /// The partial bar, or if no bar was open. + public Bar? Flush() + { + if (_bucketStart < 0) + { + return null; + } + + Bar closed = BuildBar(); + _bucketStart = -1; + return closed; + } + + /// + /// Floors a timestamp to its bar bucket. The remainder is taken with floor semantics rather than C# + /// truncation so pre-epoch timestamps bucket downwards too. + /// + private long FloorToBucket(DateTimeOffset timestamp) + { + long milliseconds = timestamp.ToUnixTimeMilliseconds(); + long remainder = milliseconds % _periodMilliseconds; + if (remainder < 0) + { + remainder += _periodMilliseconds; + } + + return milliseconds - remainder; + } + + private void StartBucket(long bucket, Tick tick) + { + _bucketStart = bucket; + _open = tick.Price; + _high = tick.Price; + _low = tick.Price; + _close = tick.Price; + _volume = tick.Volume; + _tickCount = 1; + } + + private void Accumulate(Tick tick) + { + if (tick.Price > _high) + { + _high = tick.Price; + } + + if (tick.Price < _low) + { + _low = tick.Price; + } + + _close = tick.Price; + _volume += tick.Volume; + _tickCount++; + } + + private Bar BuildBar() + => new( + _symbol, + DateTimeOffset.FromUnixTimeMilliseconds(_bucketStart), + _open, + _high, + _low, + _close, + _volume, + _tickCount); +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Streaming/BoundedFanout.cs b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/BoundedFanout.cs new file mode 100644 index 00000000..092baa56 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/BoundedFanout.cs @@ -0,0 +1,164 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Threading.Channels; + +namespace TechnicalAnalysis.Samples.RealTime.Streaming; + +/// +/// Fans one producer out to N independent consumers, each with its own bounded queue. +/// +/// The item being fanned out. Reference types only, because items are shared by every +/// subscriber and must therefore be immutable in practice. +/// +/// Backpressure policy: drop oldest, never block the producer. +/// +/// Each subscriber gets a bounded channel created with +/// . When a consumer stops reading β€” a browser tab moved +/// to the background, a console client paused at a breakpoint, a socket wedged behind a full TCP window β€” +/// its queue fills and the oldest queued item is discarded to make room for the newest. +/// +/// +/// This is the right policy for a market feed and the wrong one for an order feed. A late price is worth +/// nothing: a consumer that is 40 bars behind does not want to replay 40 stale bars, it wants the current +/// one. The alternatives are both worse here. would let the +/// slowest subscriber throttle the producer and therefore every other subscriber, which is how one +/// wedged client takes down a whole feed. An unbounded channel would trade that stall for unbounded +/// memory growth, which is the same outage with a longer fuse. +/// +/// +/// Dropping is not silent: increases by one per bar, so +/// a client that cares can see the gap. Anything that must not be lost β€” fills, cancels, risk limits β€” +/// belongs on a different, acknowledged channel, not on this one. +/// +/// +/// Subscription is race free and leak free. The subscriber list is copy-on-write: mutations take a lock +/// and publish a brand new array, while reads one immutable snapshot and never +/// takes a lock. A publisher holding a snapshot that contains a just-removed channel simply writes into a +/// completed channel, and returns false. Disposing a +/// is idempotent, so the usual +/// using/finally/disconnect-handler triple call is harmless. +/// +/// +public sealed class BoundedFanout + where T : class +{ + private readonly Lock _gate = new(); + private readonly int _capacity; + private Channel[] _subscribers = []; + + /// + /// Initializes a new instance of the class. + /// + /// How many items a single subscriber may buffer before the oldest is dropped. + /// The capacity is not positive. + public BoundedFanout(int capacity) + { + ArgumentOutOfRangeException.ThrowIfLessThan(capacity, 1); + _capacity = capacity; + } + + /// + /// Gets the number of live subscribers. + /// + public int SubscriberCount => Volatile.Read(ref _subscribers).Length; + + /// + /// Adds a subscriber and hands back the handle used to read from and later detach it. + /// + /// A subscription that must be disposed when the consumer goes away. + public FanoutSubscription Subscribe() + { + BoundedChannelOptions options = new(_capacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = false, + AllowSynchronousContinuations = false + }; + + Channel channel = Channel.CreateBounded(options); + + lock (_gate) + { + _subscribers = [.. _subscribers, channel]; + } + + return new FanoutSubscription(this, channel); + } + + /// + /// Offers an item to every live subscriber. Never blocks and never throws. + /// + /// The item to publish. + public void Publish(T item) + { + Channel[] snapshot = Volatile.Read(ref _subscribers); + foreach (Channel channel in snapshot) + { + // TryWrite returns false for a channel completed by a concurrent unsubscribe, and drops the + // oldest queued item for a channel whose consumer has fallen behind. Both are expected. + channel.Writer.TryWrite(item); + } + } + + /// + /// Detaches a subscriber and completes its channel so a pending read finishes instead of hanging. + /// + /// The channel to detach. + internal void Remove(Channel channel) + { + lock (_gate) + { + int index = Array.IndexOf(_subscribers, channel); + if (index < 0) + { + return; + } + + Channel[] replacement = new Channel[_subscribers.Length - 1]; + Array.Copy(_subscribers, replacement, index); + Array.Copy(_subscribers, index + 1, replacement, index, _subscribers.Length - index - 1); + _subscribers = replacement; + } + + channel.Writer.TryComplete(); + } +} + +/// +/// One consumer's view of a : a reader plus the right to leave. +/// +/// The item being fanned out. +public sealed class FanoutSubscription : IDisposable + where T : class +{ + private readonly BoundedFanout _owner; + private readonly Channel _channel; + private int _disposed; + + internal FanoutSubscription(BoundedFanout owner, Channel channel) + { + _owner = owner; + _channel = channel; + } + + /// + /// Gets the reader this consumer should drain. It completes when the subscription is disposed. + /// + public ChannelReader Reader => _channel.Reader; + + /// + /// Detaches from the fanout. Safe to call from any thread and safe to call more than once. + /// + public void Dispose() + { + if (Interlocked.Exchange(ref _disposed, 1) == 0) + { + _owner.Remove(_channel); + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Streaming/IndicatorPipeline.cs b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/IndicatorPipeline.cs new file mode 100644 index 00000000..7f3b4ef3 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/IndicatorPipeline.cs @@ -0,0 +1,223 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Threading.Channels; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; +using TechnicalAnalysis.Samples.RealTime.Configuration; +using TechnicalAnalysis.Samples.RealTime.Contracts; +using TechnicalAnalysis.Samples.RealTime.Diagnostics; +using TechnicalAnalysis.Samples.RealTime.Hubs; +using TechnicalAnalysis.Samples.RealTime.Indicators; + +namespace TechnicalAnalysis.Samples.RealTime.Streaming; + +/// +/// The one place ticks become indicator snapshots: tick in, bar out, snapshot out, fanned to both transports. +/// +/// +/// +/// A single loop owns every aggregator and engine, so those types stay simple, single-threaded and free of +/// locks. The loop is a subscriber of the feed like any other, which means it is subject to the same +/// drop-oldest policy β€” if this loop ever fell behind the feed it would skip ticks rather than stall the +/// producer, and the skipped ticks would show up as thinner bars rather than as a hung server. +/// +/// +/// The tick loop never awaits a client. The pull transport is a +/// write that cannot block. The SignalR push transport can: a group send +/// completes only once every member connection's transport pipe has accepted the frame, so one subscriber +/// that stops draining would otherwise park this loop and starve every symbol on every +/// transport β€” exactly the outage the drop-oldest policy exists to prevent. The group send therefore runs on +/// its own pump, fed by a bounded drop-oldest queue and bounded again by per bar. +/// A wedged SignalR client costs its own group the bar and nothing else. +/// +/// +public sealed class IndicatorPipeline : BackgroundService +{ + /// + /// How long one SignalR group send may take before the bar is abandoned for that group. Short on + /// purpose: a bar that has not reached a client within a fraction of the bar period is already stale, + /// and the next one is on its way. + /// + private static readonly TimeSpan PushTimeout = TimeSpan.FromMilliseconds(250); + + private readonly SyntheticMarketDataFeed _feed; + private readonly SnapshotBroadcaster _broadcaster; + private readonly IHubContext _hub; + private readonly RealTimeOptions _options; + private readonly ILogger _logger; + private readonly Channel _pushQueue; + private long _barsClosed; + private long _pushesDropped; + + /// + /// Initializes a new instance of the class. + /// + /// The synthetic tick source. + /// The snapshot distribution point. + /// The SignalR hub context used for the push transport. + /// The bound sample options. + /// The logger used for lifecycle messages. + public IndicatorPipeline( + SyntheticMarketDataFeed feed, + SnapshotBroadcaster broadcaster, + IHubContext hub, + IOptions options, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(options); + + _feed = feed; + _broadcaster = broadcaster; + _hub = hub; + _options = options.Value; + _logger = logger; + + // Same capacity and same policy as every other consumer queue in the sample: a late bar is worth + // nothing, so the oldest is discarded rather than letting the queue β€” or the tick loop β€” grow. + _pushQueue = Channel.CreateBounded( + new BoundedChannelOptions(_options.SubscriberQueueCapacity) + { + FullMode = BoundedChannelFullMode.DropOldest, + SingleReader = true, + SingleWriter = true, + AllowSynchronousContinuations = false + }); + } + + /// + /// Gets the number of bars closed since the process started. + /// + public long BarsClosed => Interlocked.Read(ref _barsClosed); + + /// + /// Gets the number of bars the SignalR push transport failed to deliver within + /// or dropped from its queue. Non-zero means a group had a subscriber that stopped reading. + /// + public long PushesDropped => Interlocked.Read(ref _pushesDropped); + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Subscribing before the first await guarantees no tick published after this point is missed. + using FanoutSubscription subscription = _feed.Subscribe(); + + TimeSpan barPeriod = TimeSpan.FromSeconds(_options.BarSeconds); + Dictionary pipelines = new(StringComparer.Ordinal); + int barsRequired = RollingIndicatorEngine.LargestLookback(_options.Indicators) + 1; + + foreach (string symbol in _feed.Symbols) + { + pipelines[symbol] = new SymbolPipeline( + new BarAggregator(symbol, barPeriod), + new RollingIndicatorEngine(symbol, _options.WindowSize, _options.Indicators)); + } + + SampleLog.PipelineStarted(_logger, _options.BarSeconds, _options.WindowSize, barsRequired); + + // Started before the read loop so no queued snapshot waits for a pump that does not exist yet. + Task pushPump = PushLoopAsync(); + + try + { + await foreach (Tick tick in subscription.Reader.ReadAllAsync(stoppingToken)) + { + if (!pipelines.TryGetValue(tick.Symbol, out SymbolPipeline? pipeline)) + { + continue; + } + + Bar? closed = pipeline.Aggregator.Add(tick); + if (closed is null) + { + continue; + } + + Publish(pipeline, closed); + } + } + catch (OperationCanceledException) + { + // Expected on shutdown. + } + + // Shutdown: close whatever bar is still forming so the last partial period is published rather than + // silently discarded. Cancellation is already requested at this point, so the publish path uses + // CancellationToken.None and relies on the push pump's own timeout to stay bounded. + foreach (SymbolPipeline pipeline in pipelines.Values) + { + if (pipeline.Aggregator.Flush() is { } partial) + { + Publish(pipeline, partial); + } + } + + _pushQueue.Writer.TryComplete(); + await pushPump; + + SampleLog.PipelineStopped(_logger, BarsClosed); + } + + private void Publish(SymbolPipeline pipeline, Bar bar) + { + IndicatorSnapshot snapshot = pipeline.Engine.Update(bar); + Interlocked.Increment(ref _barsClosed); + + // Pull transport: anything reading a channel (raw WebSocket clients, hub streaming callers). + _broadcaster.Publish(snapshot); + + // Push transport: handed to the pump rather than awaited here. TryWrite never blocks and never + // throws; a full queue drops the oldest snapshot, which is the same bargain the fanout makes. + if (!_pushQueue.Writer.TryWrite(snapshot)) + { + Interlocked.Increment(ref _pushesDropped); + } + + SampleLog.BarClosed( + _logger, + snapshot.Symbol, + snapshot.Sequence, + snapshot.Close, + snapshot.BarsInWindow, + snapshot.BarsRequired, + snapshot.Signal); + } + + /// + /// Drains the push queue and broadcasts each snapshot to its SignalR group, giving every send a deadline + /// so that a connection whose transport pipe has stopped draining cannot park this pump either. + /// + private async Task PushLoopAsync() + { + try + { + await foreach (IndicatorSnapshot snapshot in _pushQueue.Reader.ReadAllAsync(CancellationToken.None)) + { + using CancellationTokenSource send = new(PushTimeout); + + try + { + await _hub.Clients + .Group(IndicatorHub.GroupFor(snapshot.Symbol)) + .SendAsync(IndicatorHub.SnapshotMethod, snapshot, send.Token); + } + catch (OperationCanceledException) + { + // The deadline expired: abandon this bar for this group. Sequence numbers are contiguous + // per symbol, so a client that cares can see the gap exactly as it would a fanout drop. + Interlocked.Increment(ref _pushesDropped); + SampleLog.PushTimedOut(_logger, snapshot.Symbol, snapshot.Sequence, PushTimeout.TotalMilliseconds); + } + } + } + catch (ChannelClosedException) + { + // The writer completed while a read was pending. Nothing left to push. + } + } + + private sealed record SymbolPipeline(BarAggregator Aggregator, RollingIndicatorEngine Engine); +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Streaming/SnapshotBroadcaster.cs b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/SnapshotBroadcaster.cs new file mode 100644 index 00000000..aef92e2a --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/SnapshotBroadcaster.cs @@ -0,0 +1,147 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Collections.Concurrent; +using Microsoft.Extensions.Options; +using TechnicalAnalysis.Samples.RealTime.Configuration; +using TechnicalAnalysis.Samples.RealTime.Contracts; + +namespace TechnicalAnalysis.Samples.RealTime.Streaming; + +/// +/// The per-symbol distribution point for indicator snapshots, shared by the SignalR hub and the raw +/// WebSocket handler. +/// +/// +/// Keeping the last published snapshot per symbol is what stops a client staring at an empty chart for a +/// whole bar period after it connects. New subscribers are handed that snapshot immediately and then join +/// the live stream, so "connected" and "showing data" happen at the same moment. +/// +public sealed class SnapshotBroadcaster +{ + private readonly ConcurrentDictionary> _fanouts; + private readonly ConcurrentDictionary _latest; + private readonly ConcurrentDictionary<(string ConnectionId, string Symbol), byte> _groupMembers = new(); + private readonly HashSet _knownSymbols; + private readonly int _capacity; + + /// + /// Initializes a new instance of the class. + /// + /// The bound sample options. + public SnapshotBroadcaster(IOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + RealTimeOptions value = options.Value; + _capacity = value.SubscriberQueueCapacity; + _knownSymbols = new HashSet(value.Symbols, StringComparer.OrdinalIgnoreCase); + _fanouts = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + _latest = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + Symbols = [.. value.Symbols]; + } + + /// + /// Gets the symbols this broadcaster accepts, in configuration order. + /// + public IReadOnlyList Symbols { get; } + + /// + /// Gets the number of live channel subscribers across every symbol: raw WebSocket clients and + /// hub streaming callers, i.e. everything that reads a . + /// + /// + /// This deliberately does not count the SignalR push transport, whose subscribers live in a hub group + /// and never touch a fanout. covers those. A single number over both + /// would hide a leaked group membership behind a healthy-looking zero. + /// + public int ChannelSubscriberCount => _fanouts.Values.Sum(fanout => fanout.SubscriberCount); + + /// + /// Gets the number of live SignalR group memberships: one per connection per symbol subscribed through + /// the push transport. + /// + public int GroupSubscriberCount => _groupMembers.Count; + + /// + /// Resolves a client-supplied symbol to its configured spelling. + /// + /// The requested symbol. Case insensitive; surrounding whitespace is ignored. + /// The configured spelling, when the symbol is known. + /// when the symbol is published by this server. + public bool TryResolveSymbol(string? symbol, out string resolved) + { + string candidate = symbol?.Trim() ?? string.Empty; + if (candidate.Length > 0 && _knownSymbols.Contains(candidate)) + { + resolved = Symbols.First(known => string.Equals(known, candidate, StringComparison.OrdinalIgnoreCase)); + return true; + } + + resolved = string.Empty; + return false; + } + + /// + /// Publishes a snapshot to every subscriber of its symbol. + /// + /// The snapshot to publish. + public void Publish(IndicatorSnapshot snapshot) + { + ArgumentNullException.ThrowIfNull(snapshot); + + _latest[snapshot.Symbol] = snapshot; + FanoutFor(snapshot.Symbol).Publish(snapshot); + } + + /// + /// Subscribes to one symbol's snapshots. + /// + /// The symbol to follow. Must already have been resolved. + /// A subscription the caller must dispose when it stops reading. + public FanoutSubscription Subscribe(string symbol) => FanoutFor(symbol).Subscribe(); + + /// + /// Records that a connection joined a symbol's push group. + /// + /// The SignalR connection id. + /// The resolved symbol. + public void TrackGroupJoin(string connectionId, string symbol) => _groupMembers[(connectionId, symbol)] = 0; + + /// + /// Records that a connection left a symbol's push group. + /// + /// The SignalR connection id. + /// The resolved symbol. + public void TrackGroupLeave(string connectionId, string symbol) => _groupMembers.TryRemove((connectionId, symbol), out _); + + /// + /// Forgets every group membership of a connection. Called when the connection ends, because SignalR + /// removes a dead connection from its groups without telling the application which ones they were. + /// + /// The SignalR connection id. + public void TrackConnectionClosed(string connectionId) + { + foreach ((string ConnectionId, string Symbol) key in _groupMembers.Keys) + { + if (string.Equals(key.ConnectionId, connectionId, StringComparison.Ordinal)) + { + _groupMembers.TryRemove(key, out _); + } + } + } + + /// + /// Gets the most recently published snapshot for a symbol. + /// + /// The symbol to look up. + /// The snapshot, or if no bar has closed yet. + public IndicatorSnapshot? Latest(string symbol) + => _latest.TryGetValue(symbol, out IndicatorSnapshot? snapshot) ? snapshot : null; + + private BoundedFanout FanoutFor(string symbol) + => _fanouts.GetOrAdd(symbol, static (_, capacity) => new BoundedFanout(capacity), _capacity); +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/Streaming/SyntheticMarketDataFeed.cs b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/SyntheticMarketDataFeed.cs new file mode 100644 index 00000000..64d6c8d1 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/Streaming/SyntheticMarketDataFeed.cs @@ -0,0 +1,190 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using Microsoft.Extensions.Options; +using TechnicalAnalysis.Samples.RealTime.Configuration; +using TechnicalAnalysis.Samples.RealTime.Contracts; +using TechnicalAnalysis.Samples.RealTime.Diagnostics; + +namespace TechnicalAnalysis.Samples.RealTime.Streaming; + +/// +/// Generates a deterministic synthetic tick stream and fans it out to every subscriber. +/// +/// +/// +/// The sample must run offline, in CI, on a plane, with no API key and no market open, and it must +/// produce the same prices every time so a reader can compare their output with the README's. Every +/// symbol therefore walks a seeded mean-reverting geometric random walk: the log price takes a Gaussian +/// step and is pulled gently back towards a base level derived from the symbol name. Seeded +/// is stable across runs, machines and platforms, so +/// fully determines the price path. +/// +/// +/// Determinism covers prices, not wall-clock timing. Tick timestamps come from the real clock, so which +/// ticks land in which bar depends on timer jitter and machine load. If you need bar contents to be +/// reproducible to the cent, drive the aggregator from a virtual clock instead. +/// +/// +/// The feed is registered once as a singleton and again as a hosted service, so callers can +/// to the very same instance the background loop publishes into. +/// +/// +public sealed class SyntheticMarketDataFeed : BackgroundService +{ + private readonly BoundedFanout _fanout; + private readonly SymbolWalk[] _walks; + private readonly TimeSpan _interval; + private readonly ILogger _logger; + private long _publishedTicks; + + /// + /// Initializes a new instance of the class. + /// + /// The bound sample options. + /// The logger used for lifecycle messages. + public SyntheticMarketDataFeed(IOptions options, ILogger logger) + { + ArgumentNullException.ThrowIfNull(options); + + RealTimeOptions value = options.Value; + _logger = logger; + _interval = TimeSpan.FromMilliseconds(value.TickIntervalMilliseconds); + _fanout = new BoundedFanout(value.SubscriberQueueCapacity); + Symbols = [.. value.Symbols]; + _walks = new SymbolWalk[Symbols.Count]; + + for (int i = 0; i < Symbols.Count; i++) + { + _walks[i] = new SymbolWalk(Symbols[i], value); + } + } + + /// + /// Gets the symbols this feed publishes, in configuration order. + /// + public IReadOnlyList Symbols { get; } + + /// + /// Gets the number of ticks generated since the process started. + /// + public long PublishedTicks => Interlocked.Read(ref _publishedTicks); + + /// + /// Gets the number of live tick subscribers. + /// + public int SubscriberCount => _fanout.SubscriberCount; + + /// + /// Subscribes to the tick stream. + /// + /// A subscription the caller must dispose when it stops reading. + public FanoutSubscription Subscribe() => _fanout.Subscribe(); + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + SampleLog.FeedStarting(_logger, Symbols.Count, _interval.TotalMilliseconds); + + using PeriodicTimer timer = new(_interval); + + try + { + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + DateTimeOffset timestamp = DateTimeOffset.UtcNow; + foreach (SymbolWalk walk in _walks) + { + _fanout.Publish(walk.Next(timestamp)); + Interlocked.Increment(ref _publishedTicks); + } + } + } + catch (OperationCanceledException) + { + // Expected on shutdown. + } + + SampleLog.FeedStopped(_logger, PublishedTicks); + } + + /// + /// One symbol's seeded price process. + /// + private sealed class SymbolWalk + { + private readonly Random _random; + private readonly string _symbol; + private readonly double _basePrice; + private readonly double _volatility; + private readonly double _meanReversion; + private double _price; + private double? _spareGaussian; + + public SymbolWalk(string symbol, RealTimeOptions options) + { + _symbol = symbol; + _volatility = options.Volatility; + _meanReversion = options.MeanReversion; + + // Mixing the seed with a stable hash of the symbol gives every instrument its own path while + // keeping the whole feed reproducible from one number. string.GetHashCode is randomised per + // process, so it deliberately is not used here. + int symbolSeed = unchecked((options.RandomSeed * 397) ^ (int)StableHash(symbol)); + _random = new Random(symbolSeed); + + // Base prices land in [40, 360) so the symbols are visually distinct on a shared axis. + _basePrice = 40.0 + (StableHash(symbol) % 320u); + _price = _basePrice; + } + + public Tick Next(DateTimeOffset timestamp) + { + double pull = _meanReversion * Math.Log(_basePrice / _price); + double shock = _volatility * NextGaussian(); + _price *= Math.Exp(pull - (0.5 * _volatility * _volatility) + shock); + _price = Math.Clamp(_price, 1.0, 1_000_000.0); + + decimal price = Math.Round((decimal)_price, 2, MidpointRounding.AwayFromZero); + long volume = _random.Next(50, 5_000); + + return new Tick(_symbol, timestamp, price, volume); + } + + /// + /// FNV-1a over the symbol's characters: a stable, process-independent hash. + /// + private static uint StableHash(string value) + { + uint hash = 2166136261u; + foreach (char character in value.ToUpperInvariant()) + { + hash = unchecked((hash ^ character) * 16777619u); + } + + return hash; + } + + /// + /// Box-Muller transform, caching the second variate the polar form produces for free. + /// + private double NextGaussian() + { + if (_spareGaussian is { } spare) + { + _spareGaussian = null; + return spare; + } + + double u1 = 1.0 - _random.NextDouble(); + double u2 = _random.NextDouble(); + double magnitude = Math.Sqrt(-2.0 * Math.Log(u1)); + + _spareGaussian = magnitude * Math.Sin(2.0 * Math.PI * u2); + return magnitude * Math.Cos(2.0 * Math.PI * u2); + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/TechnicalAnalysis.Samples.RealTime.csproj b/samples/TechnicalAnalysis.Samples.RealTime/TechnicalAnalysis.Samples.RealTime.csproj new file mode 100644 index 00000000..4e9c06d9 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/TechnicalAnalysis.Samples.RealTime.csproj @@ -0,0 +1,13 @@ + + + + false + TechnicalAnalysis.Samples.RealTime + + + + + + + + diff --git a/samples/TechnicalAnalysis.Samples.RealTime/WebSockets/IndicatorWebSocketHandler.cs b/samples/TechnicalAnalysis.Samples.RealTime/WebSockets/IndicatorWebSocketHandler.cs new file mode 100644 index 00000000..ea70e3bd --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/WebSockets/IndicatorWebSocketHandler.cs @@ -0,0 +1,294 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Net.WebSockets; +using System.Text.Json; +using System.Threading.Channels; +using Microsoft.Extensions.Options; +using TechnicalAnalysis.Samples.RealTime.Configuration; +using TechnicalAnalysis.Samples.RealTime.Contracts; +using TechnicalAnalysis.Samples.RealTime.Diagnostics; +using TechnicalAnalysis.Samples.RealTime.Streaming; + +namespace TechnicalAnalysis.Samples.RealTime.WebSockets; + +/// +/// Serves the raw /ws/indicators socket: the same snapshots as the hub, with no SignalR client +/// required. +/// +/// +/// +/// SignalR is the better default for .NET clients, but a raw socket is the lowest common denominator β€” +/// it is what the sample's own page uses, what a Python or Go consumer would reach for, and what you +/// fall back to when a proxy mangles the SignalR negotiate request. Frames are newline-free JSON objects +/// of : one session frame, then a snapshot frame per closed bar. +/// +/// +/// The two halves of a socket are independent, so this runs a receive loop concurrently with a send loop. +/// The receive loop exists even though the protocol is server-to-client only: without it the close +/// handshake never completes, and a "read-only" socket is exactly how a server leaks connections that +/// look alive to it and dead to everyone else. +/// +/// +public sealed class IndicatorWebSocketHandler +{ + private const int ReceiveBufferSize = 1024; + + private static readonly TimeSpan CloseHandshakeTimeout = TimeSpan.FromSeconds(5); + + private readonly SnapshotBroadcaster _broadcaster; + private readonly RealTimeOptions _options; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The snapshot distribution point. + /// The bound sample options. + /// The logger used for connection lifecycle messages. + public IndicatorWebSocketHandler( + SnapshotBroadcaster broadcaster, + IOptions options, + ILogger logger) + { + ArgumentNullException.ThrowIfNull(options); + + _broadcaster = broadcaster; + _options = options.Value; + _logger = logger; + } + + /// + /// Runs one client connection to completion. + /// + /// The accepted socket. + /// + /// The symbol query string value. Omitting it subscribes to the server's first symbol, so + /// /ws/indicators works with no query string at all; supplying an unknown one is an error. + /// + /// The request's abort token, which fires if the connection drops. + /// A task that completes once the socket is closed. + public async Task HandleAsync(WebSocket socket, string? requestedSymbol, CancellationToken requestAborted) + { + ArgumentNullException.ThrowIfNull(socket); + + string requested = string.IsNullOrWhiteSpace(requestedSymbol) ? _broadcaster.Symbols[0] : requestedSymbol; + + if (!_broadcaster.TryResolveSymbol(requested, out string symbol)) + { + string known = string.Join(", ", _broadcaster.Symbols); + await SendAsync(socket, StreamMessage.ForError($"Unknown symbol. This server publishes: {known}."), requestAborted); + + // Nothing else owns the read side on this path, so the full handshake is safe here. + await CloseSafeAsync(socket, WebSocketCloseStatus.PolicyViolation, "Unknown symbol"); + return; + } + + // One token covers both loops: whichever finishes first cancels the other, so a client that goes + // away mid-bar tears the whole connection down instead of leaving a send loop parked on a read. + using CancellationTokenSource connection = CancellationTokenSource.CreateLinkedTokenSource(requestAborted); + using FanoutSubscription subscription = _broadcaster.Subscribe(symbol); + + SampleLog.WebSocketSubscribed(_logger, symbol); + + Task receiveLoop = ReceiveLoopAsync(socket, connection); + try + { + await SendAsync(socket, StreamMessage.ForSession(BuildSession(symbol)), connection.Token); + + // Subscribing before the replay closes the loss window and opens a duplication one: a publish + // that interleaves between the broadcaster storing "latest" and fanning out would deliver the + // same bar twice, with the same Sequence. The send loop skips anything not strictly newer. + long lastSequence = -1; + + if (_broadcaster.Latest(symbol) is { } latest) + { + lastSequence = latest.Sequence; + await SendAsync(socket, StreamMessage.ForSnapshot(latest), connection.Token); + } + + await SendLoopAsync(socket, subscription.Reader, lastSequence, connection.Token); + } + catch (OperationCanceledException) + { + // The client left, or the host is shutting down. + } + catch (WebSocketException exception) + { + SampleLog.WebSocketDropped(_logger, symbol, exception.Message); + } + finally + { + // Send our half of the close handshake without touching the read side, which the receive loop + // owns: WebSocket.CloseAsync would issue its own receive and collide with it. + await CloseOutputSafeAsync(socket, WebSocketCloseStatus.NormalClosure, "Stream finished"); + + // Give the client a moment to answer with its close frame, then stop waiting for a peer that + // is evidently not coming back. + if (await Task.WhenAny(receiveLoop, Task.Delay(CloseHandshakeTimeout, CancellationToken.None)) != receiveLoop) + { + await connection.CancelAsync(); + } + + await receiveLoop; + SampleLog.WebSocketDisconnected(_logger, symbol); + } + } + + /// + /// Pumps snapshots to the client. The only writer to the socket, so sends never overlap. + /// + /// The client socket. + /// This client's snapshot queue. + /// + /// The highest sequence already sent during the replay, or -1 when nothing was replayed. + /// + /// Fires when the client leaves or the host shuts down. + private static async Task SendLoopAsync( + WebSocket socket, + ChannelReader reader, + long lastSequence, + CancellationToken cancellationToken) + { + await foreach (IndicatorSnapshot snapshot in reader.ReadAllAsync(cancellationToken)) + { + if (socket.State != WebSocketState.Open) + { + return; + } + + // Strictly increasing, so the replayed snapshot is never sent twice. A gap still means a + // genuine drop-oldest discard, which is what the client's gap counter is for. + if (snapshot.Sequence <= lastSequence) + { + continue; + } + + lastSequence = snapshot.Sequence; + await SendAsync(socket, StreamMessage.ForSnapshot(snapshot), cancellationToken); + } + } + + /// + /// Drains whatever the client sends and, more importantly, notices when it sends a close frame. + /// + /// + /// The sample's protocol is one-way, so any payload is discarded; a real feed would parse subscribe + /// and unsubscribe commands here. Receiving the close frame is what lets + /// complete the handshake rather than time out. + /// + private static async Task ReceiveLoopAsync(WebSocket socket, CancellationTokenSource connection) + { + byte[] buffer = new byte[ReceiveBufferSize]; + + try + { + while (socket.State == WebSocketState.Open && !connection.Token.IsCancellationRequested) + { + WebSocketReceiveResult result = await socket.ReceiveAsync(buffer, connection.Token); + if (result.MessageType == WebSocketMessageType.Close) + { + return; + } + } + } + catch (OperationCanceledException) + { + // Expected once the send side finishes or the request is aborted. + } + catch (WebSocketException) + { + // The peer vanished without a close frame. Nothing to salvage. + } + finally + { + await connection.CancelAsync(); + } + } + + private static async Task SendAsync(WebSocket socket, StreamMessage message, CancellationToken cancellationToken) + { + if (socket.State != WebSocketState.Open) + { + return; + } + + byte[] payload = JsonSerializer.SerializeToUtf8Bytes(message, RealTimeJsonContext.Default.StreamMessage); + await socket.SendAsync(payload, WebSocketMessageType.Text, endOfMessage: true, cancellationToken); + } + + /// + /// Sends the server's close frame. Safe to call in any state and never throws. + /// + /// + /// This is the send half only. It works whether the client closed first (state + /// ) or the server did (state ), + /// and it never competes with the receive loop for the read side. + /// + private static async Task CloseOutputSafeAsync(WebSocket socket, WebSocketCloseStatus status, string description) + { + if (socket.State is not (WebSocketState.Open or WebSocketState.CloseReceived)) + { + return; + } + + try + { + await socket.CloseOutputAsync(status, description, CancellationToken.None); + } + catch (WebSocketException) + { + // The peer is already gone; there is nothing left to hand shake with. + } + catch (ObjectDisposedException) + { + // The host tore the connection down first. + } + } + + /// + /// Performs the full close handshake. Only safe when nothing else is reading the socket. + /// + private static async Task CloseSafeAsync(WebSocket socket, WebSocketCloseStatus status, string description) + { + if (socket.State is not (WebSocketState.Open or WebSocketState.CloseReceived)) + { + return; + } + + try + { + await socket.CloseAsync(status, description, CancellationToken.None); + } + catch (WebSocketException) + { + // The peer is already gone; there is nothing left to hand shake with. + } + catch (ObjectDisposedException) + { + // The host tore the connection down first. + } + } + + private SessionInfo BuildSession(string symbol) + => new( + symbol, + _broadcaster.Symbols, + _options.BarSeconds, + _options.TickIntervalMilliseconds, + _options.WindowSize, + new IndicatorPeriods( + _options.Indicators.SmaFastPeriod, + _options.Indicators.SmaSlowPeriod, + _options.Indicators.EmaPeriod, + _options.Indicators.RsiPeriod, + _options.Indicators.MacdFastPeriod, + _options.Indicators.MacdSlowPeriod, + _options.Indicators.MacdSignalPeriod, + _options.Indicators.BollingerPeriod, + _options.Indicators.BollingerDeviations, + _options.Indicators.AtrPeriod)); +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/appsettings.Development.json b/samples/TechnicalAnalysis.Samples.RealTime/appsettings.Development.json new file mode 100644 index 00000000..a3d032a7 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/appsettings.Development.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "TechnicalAnalysis.Samples.RealTime": "Debug" + } + }, + "RealTime": { + "TickIntervalMilliseconds": 100, + "BarSeconds": 1 + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/appsettings.json b/samples/TechnicalAnalysis.Samples.RealTime/appsettings.json new file mode 100644 index 00000000..fcbe6e63 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/appsettings.json @@ -0,0 +1,34 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Urls": "http://localhost:5199", + "RealTime": { + "Symbols": [ "ACME", "GLOBEX", "INITECH" ], + "TickIntervalMilliseconds": 250, + "BarSeconds": 5, + "RandomSeed": 20240613, + "WindowSize": 256, + "SubscriberQueueCapacity": 64, + "Volatility": 0.0015, + "MeanReversion": 0.0025, + "Indicators": { + "SmaFastPeriod": 10, + "SmaSlowPeriod": 30, + "EmaPeriod": 20, + "RsiPeriod": 14, + "MacdFastPeriod": 12, + "MacdSlowPeriod": 26, + "MacdSignalPeriod": 9, + "BollingerPeriod": 20, + "BollingerDeviations": 2.0, + "AtrPeriod": 14, + "RsiOverbought": 70.0, + "RsiOversold": 30.0 + } + } +} diff --git a/samples/TechnicalAnalysis.Samples.RealTime/wwwroot/index.html b/samples/TechnicalAnalysis.Samples.RealTime/wwwroot/index.html new file mode 100644 index 00000000..5a4bd796 --- /dev/null +++ b/samples/TechnicalAnalysis.Samples.RealTime/wwwroot/index.html @@ -0,0 +1,739 @@ + + + + + + TaLibStandard Β· live tape + + + +
+

TaLibStandard live tape

+
+
+ + +
+
+ + connecting +
+
+ +
+
+
Last close
+
β€”
+
+
+
Change on window
+
β€”
+
+
+
Bar
+
β€”
+
+
+
Signal
+
waiting
+
+
+ +
+
+
+
+ Price Β· closed bars +
+ close + SMA fast + SMA slow +
+
+ +
+ +
+
+ Relative strength +
RSI
+
+ +
+
+ + +
+ +

+ Streaming over a raw WebSocket at /ws/indicators. The same snapshots are on SignalR at + /hubs/indicators. A dashed value has not warmed up yet: the server sends + null for it rather than a zero or a stale reading, and the hairline under the row shows + how much history it still needs. +

+ + + + diff --git a/src/TechnicalAnalysis.Functions/Adx/TAFunc.cs b/src/TechnicalAnalysis.Functions/Adx/TAFunc.cs index 204b2191..bcb4c8bc 100644 --- a/src/TechnicalAnalysis.Functions/Adx/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/Adx/TAFunc.cs @@ -81,15 +81,14 @@ public static RetCode Adx( double prevHigh = inHigh[today]; double prevLow = inLow[today]; double prevClose = inClose[today]; + // The reference implementation seeds with `while (i-- > 0)`, which executes the body + // exactly `i` times. Translating it as `while (true) { i--; if (i <= 0) break; ... }` + // executes it `i - 1` times, which leaves `today` short of startIdx and makes the tail + // loop emit that many extra values: outBegIdx + outNBElement then overruns the input. int i = optInTimePeriod - 1; - while (true) + while (i > 0) { i--; - if (i <= 0) - { - break; - } - today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; @@ -125,14 +124,9 @@ public static RetCode Adx( double sumDX = 0.0; i = optInTimePeriod; - while (true) + while (i > 0) { i--; - if (i <= 0) - { - break; - } - today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; @@ -174,14 +168,9 @@ public static RetCode Adx( double prevADX = sumDX / optInTimePeriod; i = (int)TACore.Globals.UnstablePeriod[FuncUnstId.Adx]; - while (true) + while (i > 0) { i--; - if (i <= 0) - { - break; - } - today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; diff --git a/src/TechnicalAnalysis.Functions/Atr/TAFunc.cs b/src/TechnicalAnalysis.Functions/Atr/TAFunc.cs index 0aed8185..1a547f33 100644 --- a/src/TechnicalAnalysis.Functions/Atr/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/Atr/TAFunc.cs @@ -137,10 +137,16 @@ public static RetCode Atr( break; } + // Wilder smoothing carries the *normalised* average into the next bar. Dividing only + // on the way out (outReal[outIdx] = prevATR / optInTimePeriod) left the accumulator + // multiplied by (period - 1) every bar, so ATR diverged geometrically and reached + // +Infinity within a few hundred bars. The warm-up loop above and Natr both + // normalise the running value here; this loop has to as well. prevATR *= optInTimePeriod - 1; prevATR += tempBuffer[today]; today++; - outReal[outIdx] = prevATR / optInTimePeriod; + prevATR /= optInTimePeriod; + outReal[outIdx] = prevATR; outIdx++; } diff --git a/src/TechnicalAnalysis.Functions/Dx/TAFunc.cs b/src/TechnicalAnalysis.Functions/Dx/TAFunc.cs index 2a31746f..57f18904 100644 --- a/src/TechnicalAnalysis.Functions/Dx/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/Dx/TAFunc.cs @@ -104,15 +104,14 @@ public static RetCode Dx( double prevHigh = inHigh[today]; double prevLow = inLow[today]; double prevClose = inClose[today]; + // The reference implementation seeds with `while (i-- > 0)`, which executes the body + // exactly `i` times. Translating it as `while (true) { i--; if (i <= 0) break; ... }` + // executes it `i - 1` times, which leaves `today` short of startIdx and makes the tail + // loop emit that many extra values: outBegIdx + outNBElement then overruns the input. int i = optInTimePeriod - 1; - while (true) + while (i > 0) { i--; - if (i <= 0) - { - break; - } - today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; @@ -146,15 +145,11 @@ public static RetCode Dx( prevClose = inClose[today]; } + // Likewise `while (i-- != 0)`, which executes the body exactly `i` times. i = (int)TACore.Globals.UnstablePeriod[FuncUnstId.Dx] + 1; - while (true) + while (i != 0) { i--; - if (i == 0) - { - break; - } - today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; diff --git a/src/TechnicalAnalysis.Functions/HighLevel/BollingerBandsSeries.cs b/src/TechnicalAnalysis.Functions/HighLevel/BollingerBandsSeries.cs new file mode 100644 index 00000000..4675355e --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/BollingerBandsSeries.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// The three bar-aligned bands of a Bollinger Bands calculation. +/// +/// +/// The upper band: the middle band plus the requested number of standard deviations. Bar-aligned. +/// +/// +/// The middle band: the moving average of the closing prices. Bar-aligned. +/// +/// +/// The lower band: the middle band minus the requested number of standard deviations. Bar-aligned. +/// +/// +/// Each band is an independently addressable . Derived measures such +/// as %B or bandwidth are deliberately not provided: they are arithmetic on three numbers the +/// caller already has, and every convention for them is an opinion. +/// +public readonly record struct BollingerBandsSeries(IndicatorSeries Upper, IndicatorSeries Middle, IndicatorSeries Lower) +{ + /// + /// Narrows every band so that it ends at the given BAR index. + /// + /// + /// The last BAR index the narrowed bands are allowed to know about, with domain + /// [0, BarCount) of the bands. + /// + /// A result whose three bands have each been narrowed by . + /// + /// is outside the bars the bands cover. + /// + public BollingerBandsSeries AsOf(int bar) + { + return new BollingerBandsSeries(Upper.AsOf(bar), Middle.AsOf(bar), Lower.AsOf(bar)); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/IndicatorSeries.Enumerator.cs b/src/TechnicalAnalysis.Functions/HighLevel/IndicatorSeries.Enumerator.cs new file mode 100644 index 00000000..04b03529 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/IndicatorSeries.Enumerator.cs @@ -0,0 +1,105 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Diagnostics.CodeAnalysis; + +namespace TechnicalAnalysis.Functions; + +/// +/// Contains the allocation-free enumerator over the warm bars of an . +/// +public readonly partial struct IndicatorSeries +{ + /// + /// Enumerates the warm bars of an in ascending BAR order. + /// + /// + /// + /// Each iteration yields a (Bar, Value) pair in which Bar is a BAR index into the + /// source price series β€” an absolute position, never an index into the raw TA-Lib output array. + /// Bars that have no value are skipped rather than yielded as null, so enumerating an + /// empty series performs zero iterations. + /// + /// + /// is deliberately not implemented: + /// foreach binds to this pattern directly and allocates nothing, whereas implementing the + /// interface would box the enumerator on every loop. Adding the interface later is a + /// non-breaking change; removing an allocating enumerator would not be. + /// + /// + [SuppressMessage( + "Design", + "CA1034:Nested types should not be visible", + Justification = "Standard BCL value-type enumerator pattern; enables allocation-free foreach.")] + [SuppressMessage( + "Performance", + "CA1815:Override equals and operator equals on value types", + Justification = "An enumerator is a cursor, not a value; equality is meaningless.")] + public struct Enumerator + { + /// + /// The series being enumerated. + /// + private readonly IndicatorSeries _series; + + /// + /// The ARRAY index of the current element, or -1 before the first + /// . + /// + private int _arrayIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The series to enumerate. + internal Enumerator(IndicatorSeries series) + { + _series = series; + _arrayIndex = -1; + } + + /// + /// Gets the current bar and its value. + /// + /// + /// A pair whose Bar is a BAR index in [FirstBar, LastBar] and whose + /// Value is the value at that bar. Valid only after has + /// returned true. + /// + /// + /// has not yet been called, or it has already returned + /// false. foreach never reaches this state; hand-driving the enumerator can, + /// and a out of a public API would read as a library + /// defect rather than as caller misuse. + /// + public readonly (int Bar, double Value) Current => + (uint)_arrayIndex < (uint)_series.WarmCount + ? _series.PairAt(_arrayIndex) + : throw new InvalidOperationException( + "Enumeration has either not started or has already finished; call MoveNext and check that it " + + "returned true before reading Current."); + + /// + /// Advances to the next warm bar. + /// + /// + /// true when another warm bar is available and has been + /// positioned on it; false when the series is exhausted. + /// + public bool MoveNext() + { + // The cursor is advanced one position past the last value, so that Current reports + // "already finished" rather than handing back the last pair a second time, and it then + // stops moving so that repeated calls cannot run the index away. + if (_arrayIndex < _series.WarmCount) + { + _arrayIndex++; + } + + return _arrayIndex < _series.WarmCount; + } + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/IndicatorSeries.cs b/src/TechnicalAnalysis.Functions/HighLevel/IndicatorSeries.cs new file mode 100644 index 00000000..ed4c001c --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/IndicatorSeries.cs @@ -0,0 +1,911 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace TechnicalAnalysis.Functions; + +/// +/// A bar-aligned view over a single output series produced by a TA-Lib indicator. +/// This is the one place in the library where TA-Lib's raw alignment metadata is interpreted. +/// +/// +/// +/// There are two index spaces, and confusing them is the classic TA-Lib bug. +/// +/// +/// An array index is a subscript into the raw [] that a +/// TAMath call returned. TA-Lib fills that array starting at array index 0, and it +/// writes exactly NBElement elements, so the last valid array index is +/// NBElement - 1. Everything from NBElement to the end of the allocated array is +/// untouched zero padding and means nothing. +/// +/// +/// A bar index is a position in the source price series, with domain +/// [0, BarCount). TA-Lib reports BegIdx, which is a bar index: output array +/// element k describes bar BegIdx + k. The last valid bar index is therefore +/// BegIdx + NBElement - 1. +/// +/// +/// NBElement - 1 and BegIdx + NBElement - 1 are two different numbers; they coincide +/// only when BegIdx == 0. Subscripting the output array with the bar index +/// BegIdx + NBElement - 1 lands in the zero padding and silently yields 0.0. That is +/// the mistake this type exists to make unsayable. +/// +/// +/// The rule. Every on this type that names a position is a BAR index. +/// That covers the bar parameter of , , +/// and all four crossing overloads; the values returned by +/// and ; the Bar component yielded by +/// ; and the index space of , +/// and . +/// and are counts, not indices, and neither is a +/// valid loop bound for the indexer β€” the indexer's domain is [0, BarCount), so a loop over +/// it must be bounded by . No member of this type accepts or returns a raw +/// TA-Lib output-array index, with the single deliberate exception of , +/// whose name and documentation state its index space explicitly. is the sole +/// point at which raw TA-Lib metadata enters the type system. +/// +/// +/// Absence is null, uniformly. A bar that is inside the series but before the +/// indicator has warmed up has no value, and that is reported as null β€” never 0.0, +/// never , and never an exception. There is no sentinel value anywhere on +/// this type: and are ?, +/// and and are ?. The single +/// exception is and , which pad with +/// because a [] cannot hold null; +/// is the projection that keeps the promise. +/// +/// +/// A bar outside [0, BarCount) is a caller bug and throws +/// . "Bar 5 of a 30-period SMA" is a legitimate question +/// whose answer is "no value"; "bar 5000 of a 100-bar series" is not a question at all. That split +/// is what turns into a causality guarantee rather than a convention. +/// +/// +/// is not warmth. A successful call over too little data reports +/// with BegIdx == 0 and NBElement == 0, and in +/// that state BegIdx is a lie. (equivalently +/// WarmCount > 0) is the only warmth test. default(IndicatorSeries) is a valid +/// series with no values that reports , because +/// Success == 0. +/// +/// +/// This is an immutable value type, and its immutability is unconditional: +/// copies the warm values out of the array it is handed, so no caller can reach inside a series +/// after handing it over. Every instance is therefore safe for unrestricted concurrent use. +/// +/// +/// +/// +/// PriceSeries prices = PriceSeries.FromClose(closes); +/// IndicatorSeries sma = prices.Sma(30); +/// +/// double? latest = sma.Latest; // null until the indicator has warmed up +/// double? atBar50 = sma[50]; // 50 is a BAR index, not an array index +/// if (sma[50] is { } value) +/// { +/// Console.WriteLine(value); +/// } +/// +/// +public readonly partial struct IndicatorSeries : IEquatable +{ + /// + /// The raw TA-Lib output array. Element k describes bar _firstBar + k, and only + /// the first elements are meaningful. Normalised to null + /// whenever is zero, so that every series with no values is + /// indistinguishable from every other one with the same shape. + /// + private readonly double[]? _values; + + /// + /// The BAR index described by array element 0 β€” TA-Lib's BegIdx. Meaningless + /// when is zero, and normalised to 0 in that case so that + /// equality and hashing are stable. + /// + private readonly int _firstBar; + + /// + /// Initializes a new instance of the struct. + /// + /// The return code reported by the underlying TA-Lib call. + /// The BAR index of array element 0; ignored when is zero. + /// The number of meaningful values in . + /// The raw TA-Lib output array, or null for a series with no values. + /// The number of bars in the source price series. + private IndicatorSeries(RetCode retCode, int firstBar, int count, double[]? values, int barCount) + { + bool noValues = count == 0; + RetCode = retCode; + BarCount = barCount; + WarmCount = count; + _firstBar = noValues ? 0 : firstBar; + _values = noValues ? null : values; + } + + /// + /// Gets the return code reported by the underlying TA-Lib call. + /// + /// + /// The raw TA-Lib status. This answers only "were the parameters acceptable"; it never answers + /// "are there values". A series can report and still hold + /// nothing, because a period longer than the available data is a success that produces nothing. + /// Use to test for warmth. + /// + public RetCode RetCode { get; } + + /// + /// Gets the number of bars in the source price series. + /// + /// + /// A count, not an index. The domain of every bar index on this type is + /// [0, BarCount), so this β€” not β€” is the bound of a loop over + /// . + /// + public int BarCount { get; } + + /// + /// Gets the number of bars that carry a value β€” TA-Lib's NBElement. + /// + /// + /// A count in the range [0, BarCount], expressed in ARRAY space: it is the length of + /// . It is deliberately not called Count, because on a + /// type with an indexer Count reads as "the number of valid indices" and this is not + /// that: the indexer's domain is [0, BarCount). Looping for (int i = 0; i < + /// s.WarmCount; i++) s[i] would read the wrong bars and silently drop the most recent + /// ones. The last bar that carries a value is , which is + /// FirstBar + WarmCount - 1. + /// + public int WarmCount { get; } + + /// + /// Gets a value indicating whether any bar of this series carries a value. + /// + /// + /// true when is greater than zero. This is the only warmth test; + /// a series can carry nothing and still report . It is + /// deliberately not called IsEmpty: means "no bars", + /// and a series here can carry no values while covering a hundred bars. + /// + public bool HasValues => WarmCount > 0; + + /// + /// Gets the BAR index of the first bar that has a value, or null when no bar has one. + /// + /// + /// A bar index in [0, BarCount) β€” TA-Lib's BegIdx, reinterpreted as the true + /// lookback of the indicator. null if and only if is + /// false. + /// + public int? FirstBar => WarmCount == 0 ? null : _firstBar; + + /// + /// Gets the BAR index of the last bar that has a value, or null when no bar has one. + /// + /// + /// A bar index in [0, BarCount), equal to FirstBar + WarmCount - 1. Note that + /// this is a bar index and WarmCount - 1 is an array index; they are different numbers. + /// null if and only if is false. + /// + public int? LastBar => WarmCount == 0 ? null : ToBarIndex(WarmCount - 1); + + /// + /// Gets the value at the given BAR index, or null when that bar has no value. + /// + /// + /// A BAR index into the source price series, with domain [0, BarCount). This is + /// not an index into the raw TA-Lib output array. + /// + /// + /// The value describing , or null when is + /// inside the series but outside [FirstBar, LastBar] β€” typically a bar before the + /// indicator warmed up. Never 0.0 and never for a bar that has + /// no value. + /// + /// + /// is negative or greater than or equal to . + /// Asking about a bar the series does not cover is a caller bug, whereas asking about a bar + /// that has not warmed up is a legitimate question answered with null. + /// + public double? this[int bar] + { + get + { + ValidateBar(bar); + int arrayIndex = ToArrayIndex(bar); + return IsWarmArrayIndex(arrayIndex) ? _values![arrayIndex] : null; + } + } + + /// + /// Gets the most recent value in the series, or null when no bar has a value. + /// + /// + /// The value at . It is read from array index + /// WarmCount - 1, never from bar index FirstBar + WarmCount - 1; the latter would + /// index the array with a bar index, which is the historical alignment bug. + /// + public double? Latest => WarmCount == 0 ? null : _values![WarmCount - 1]; + + /// + /// Gets the values as a span whose element k describes bar FirstBar + k. + /// + /// + /// A read-only span of exactly elements β€” the single ARRAY-indexed view + /// on this type, which is why it is named WarmValues rather than Values. It + /// starts at array index 0 and is sliced to , so TA-Lib's + /// untouched zero padding is unreachable and the historical expression + /// Values[BegIdx + NBElement - 1] throws instead + /// of silently returning a padding zero. A bar index is not a valid subscript here: use + /// for that. A series with no values yields an empty span. + /// + public ReadOnlySpan WarmValues => _values is null ? default : new ReadOnlySpan(_values, 0, WarmCount); + + /// + /// Creates a series covering the given number of bars in which no bar has a value. + /// + /// The number of bars in the source price series. Must not be negative. + /// + /// A series reporting with zero, + /// equal to , and + /// null. + /// + /// is negative. + public static IndicatorSeries Empty(int barCount) + { + ArgumentOutOfRangeException.ThrowIfNegative(barCount); + return new IndicatorSeries(Success, 0, 0, null, barCount); + } + + /// + /// Creates a bar-aligned series from the raw metadata of a TA-Lib call. This is the only point + /// in the library at which raw TA-Lib alignment metadata enters the type system. + /// + /// The return code reported by the TA-Lib call. + /// + /// TA-Lib's BegIdx: the BAR index described by output array element 0. It is not + /// examined at all when is zero, because TA-Lib reports + /// BegIdx == 0 in that state and the value is meaningless. + /// + /// + /// TA-Lib's NBElement: the number of elements it actually wrote, starting at ARRAY index + /// 0. This is a count, not an index. + /// + /// + /// The raw output array. Its first elements are copied, so + /// the caller keeps sole ownership of the array it passed and may mutate it afterwards without + /// affecting the series that was handed back. That is what makes the immutability of this type + /// unconditional rather than a convention the caller has to honour. + /// + /// The number of bars in the source price series. + /// A series in which every position is addressed by BAR index. + /// is null. + /// + /// , or is negative. + /// + /// + /// exceeds the length of , or + /// begIdx + nbElement exceeds . The second check enforces + /// TA-Lib's own invariant: the last described bar is begIdx + nbElement - 1, which must + /// fall inside the source series. A result that violates it is misaligned at its source, and + /// clamping it here would produce a silently shifted series β€” precisely the failure this type + /// exists to prevent β€” so it is surfaced loudly instead. + /// + public static IndicatorSeries Create(RetCode retCode, int begIdx, int nbElement, double[] values, int barCount) + { + ArgumentNullException.ThrowIfNull(values); + + return CreateCore(retCode, begIdx, nbElement, values, barCount, copy: true); + } + + /// + /// Determines whether two series are equal. + /// + /// The first series. + /// The second series. + /// true when the two series are equal; otherwise false. + public static bool operator ==(IndicatorSeries left, IndicatorSeries right) + { + return left.Equals(right); + } + + /// + /// Determines whether two series are not equal. + /// + /// The first series. + /// The second series. + /// true when the two series are not equal; otherwise false. + public static bool operator !=(IndicatorSeries left, IndicatorSeries right) + { + return !left.Equals(right); + } + + /// + /// Creates a bar-aligned series that takes ownership of the output array instead of copying it. + /// + /// The return code reported by the TA-Lib call. + /// TA-Lib's BegIdx: the BAR index described by array element 0. + /// TA-Lib's NBElement: the number of elements written. + /// + /// The output array, which becomes the property of the series. It must have been allocated by + /// the TAMath call being wrapped and must not be reachable from anywhere else, otherwise + /// the immutability this type promises is broken. + /// + /// The number of bars in the source price series. + /// A series in which every position is addressed by BAR index. + /// + /// Internal on purpose. The shipped fluent indicators know that the array they pass was + /// freshly allocated inside the call they just made and is held by nothing else, so they can + /// skip the copy; a caller outside the assembly cannot make that promise about + /// result.Real, which is why copies. + /// + internal static IndicatorSeries CreateOwning(RetCode retCode, int begIdx, int nbElement, double[] values, int barCount) + { + ArgumentNullException.ThrowIfNull(values); + + return CreateCore(retCode, begIdx, nbElement, values, barCount, copy: false); + } + + /// + /// Determines whether the given BAR index has a value. + /// + /// + /// A BAR index into the source price series, with domain [0, BarCount). + /// + /// + /// true when lies in [FirstBar, LastBar]; otherwise + /// false. Equivalent to this[bar] is not null, without the nullable value. + /// + /// + /// is negative or greater than or equal to . + /// + public bool IsWarmAt(int bar) + { + ValidateBar(bar); + return IsWarmArrayIndex(ToArrayIndex(bar)); + } + + /// + /// Returns the same series truncated so that it ends at the given BAR index, making look-ahead + /// unrepresentable rather than merely detectable. + /// + /// + /// The last BAR index the narrowed series is allowed to know about, with domain + /// [0, BarCount). + /// + /// + /// A series with equal to bar + 1 and the same + /// , holding only the values at bars up to and including + /// . If no value survives, the result carries none at all. Bar indices + /// are not rebased: they remain absolute positions in the original price series. + /// + /// + /// + /// Asking the narrowed series about a later bar throws, because that bar is outside its domain: + /// series.AsOf(50)[51] is an , not a value and + /// not null. The future is simply not part of the value handed over. + /// + /// + /// All shipped indicators are causal, so narrowing the end never changes an earlier value: + /// prices.AsOf(bar).Sma(30).Latest equals prices.Sma(30).AsOf(bar).Latest + /// exactly. This is the reason to prefer computing once and narrowing per bar β€” which is + /// allocation-free and O(1) β€” over recomputing the indicator inside a per-bar loop, which is + /// O(nΒ²). + /// + /// + /// + /// is negative or greater than or equal to . + /// + public IndicatorSeries AsOf(int bar) + { + ValidateBar(bar); + + int survivingCount = WarmCount == 0 ? 0 : Math.Clamp(ToArrayIndex(bar) + 1, 0, WarmCount); + + return new IndicatorSeries(RetCode, _firstBar, survivingCount, _values, bar + 1); + } + + /// + /// Determines whether this series crossed above a fixed level at the given BAR index. + /// + /// The level to test against. + /// + /// The BAR index at which the crossing is tested, with domain [0, BarCount). + /// + /// + /// true when the value at is strictly above + /// and the value at bar - 1 was at or below it. A crossing is a + /// transition between two bars, not a state: a series that is already above the level does not + /// keep reporting a crossing. Returns false when is 0, or + /// when either bar has no value. + /// + /// + /// is negative or greater than or equal to . + /// + public bool CrossedAbove(double level, int bar) + { + ValidateBar(bar); + + return bar > 0 + && this[bar] is { } current + && this[bar - 1] is { } previous + && current > level + && previous <= level; + } + + /// + /// Determines whether this series crossed below a fixed level at the given BAR index. + /// + /// The level to test against. + /// + /// The BAR index at which the crossing is tested, with domain [0, BarCount). + /// + /// + /// true when the value at is strictly below + /// and the value at bar - 1 was at or above it. Returns + /// false when is 0, or when either bar has no value. + /// + /// + /// is negative or greater than or equal to . + /// + public bool CrossedBelow(double level, int bar) + { + ValidateBar(bar); + + return bar > 0 + && this[bar] is { } current + && this[bar - 1] is { } previous + && current < level + && previous >= level; + } + + /// + /// Determines whether this series crossed above another series at the given BAR index. + /// + /// + /// The series to cross against. It must cover the same number of bars as this one, so that a + /// bar index means the same thing in both. + /// + /// + /// The BAR index at which the crossing is tested, with domain [0, BarCount). + /// + /// + /// true when this series is strictly above at + /// and was at or below it at bar - 1. Returns false when + /// is 0, or when any of the four values involved is missing β€” + /// which is what makes a fast/slow crossing with different warm-ups work without any reasoning + /// at the call site. + /// + /// + /// covers a different number of bars than this series. Crossing two + /// series computed over different price series is a caller bug. + /// + /// + /// is negative or greater than or equal to . + /// + public bool CrossedAbove(IndicatorSeries other, int bar) + { + ValidateComparable(other); + ValidateBar(bar); + + return bar > 0 + && this[bar] is { } current + && this[bar - 1] is { } previous + && other[bar] is { } otherCurrent + && other[bar - 1] is { } otherPrevious + && current > otherCurrent + && previous <= otherPrevious; + } + + /// + /// Determines whether this series crossed below another series at the given BAR index. + /// + /// + /// The series to cross against. It must cover the same number of bars as this one. + /// + /// + /// The BAR index at which the crossing is tested, with domain [0, BarCount). + /// + /// + /// true when this series is strictly below at + /// and was at or above it at bar - 1. Returns false when + /// is 0, or when any of the four values involved is missing. + /// + /// + /// covers a different number of bars than this series. + /// + /// + /// is negative or greater than or equal to . + /// + public bool CrossedBelow(IndicatorSeries other, int bar) + { + ValidateComparable(other); + ValidateBar(bar); + + return bar > 0 + && this[bar] is { } current + && this[bar - 1] is { } previous + && other[bar] is { } otherCurrent + && other[bar - 1] is { } otherPrevious + && current < otherCurrent + && previous >= otherPrevious; + } + + /// + /// Projects the series onto a new array indexed by BAR index, padding bars that have no value + /// with . + /// + /// + /// A new array of exactly elements in which element i describes + /// bar i. The padding value is and is deliberately not + /// configurable, because padding with 0.0 reinstates exactly the silent corruption this + /// type exists to prevent. + /// + /// + /// + /// is a sentinel here, and it is the one place on this type where + /// absence is not null. A [] cannot hold null, so + /// this projection collapses "this bar has no value" and "this bar has a value and the value is + /// not finite" onto the same bit pattern. The fluent factories reject non-finite prices, so a + /// series produced through this API cannot contain a computed ; a + /// series built through from a hand-rolled call can. When the two states + /// must be told apart, use , or + /// , none of which have a sentinel. + /// + /// + /// Because every indicator has a warm-up, the result of this method almost always contains + /// : ToBarAlignedArray().Max() and .Average() are + /// for a typical series, and a chart fed the raw array must be told + /// how to skip them. + /// + /// + public double[] ToBarAlignedArray() + { + double[] result = new double[BarCount]; + WriteBarAligned(result); + return result; + } + + /// + /// Projects the series onto a new array indexed by BAR index, with null for bars that + /// have no value. + /// + /// + /// A new array of exactly elements in which element i describes + /// bar i, and a bar with no value is null rather than a sentinel. + /// + /// + /// This is the projection that keeps the type's central promise β€” absence is null β€” at + /// the cost of a boxed-free but larger array. Prefer + /// only when the consumer needs a contiguous + /// [] and already understands the + /// convention. + /// + public double?[] ToBarAlignedNullableArray() + { + double?[] result = new double?[BarCount]; + + for (int arrayIndex = 0; arrayIndex < WarmCount; arrayIndex++) + { + result[ToBarIndex(arrayIndex)] = _values![arrayIndex]; + } + + return result; + } + + /// + /// Copies the values into a new array whose element k describes bar + /// FirstBar + k. + /// + /// + /// A new array of exactly elements, ARRAY-indexed exactly as + /// is. It exists because is a + /// and therefore cannot escape into a LINQ query, an + /// async method or a field. + /// + public double[] WarmValuesToArray() + { + return WarmCount == 0 ? [] : _values![..WarmCount]; + } + + /// + /// Writes the series into the given span in which the index is the BAR index. + /// + /// + /// The span to write into. Its first elements are overwritten, so that + /// element i describes bar i; bars that have no value are written as + /// . Any elements beyond are left untouched. + /// + /// + /// The padding carries the same caveat as + /// : it is a sentinel, and it cannot be told apart from a + /// computed non-finite value by inspecting the destination alone. + /// + /// + /// is shorter than . + /// + public void CopyBarAligned(Span destination) + { + if (destination.Length < BarCount) + { + throw new ArgumentException( + string.Create( + CultureInfo.InvariantCulture, + $"The destination holds {destination.Length} element(s) but {BarCount} are needed, one per bar."), + nameof(destination)); + } + + WriteBarAligned(destination); + } + + /// + /// Returns an allocation-free enumerator over the bars of this series that carry a value. + /// + /// + /// An enumerator yielding (Bar, Value) pairs in ascending BAR order, one per bar with a + /// value, where Bar is a BAR index in [FirstBar, LastBar]. Bars with no value are + /// skipped entirely, so enumerating a series with no values performs zero iterations. + /// + public Enumerator GetEnumerator() + { + return new Enumerator(this); + } + + /// + /// Determines whether this series equals another. + /// + /// The series to compare with. + /// + /// true when both series share the same backing array by reference and agree on + /// their first bar, value count, bar count and return code. + /// + /// + /// This does not compare values. Two series computed separately from identical inputs + /// are not equal, because they wrap different arrays. Equality exists so that this value type + /// satisfies CA1815 and so that can be recognised as the identity when it + /// narrows nothing; it is not a numeric comparison. + /// + public bool Equals(IndicatorSeries other) + { + return ReferenceEquals(_values, other._values) + && _firstBar == other._firstBar + && WarmCount == other.WarmCount + && BarCount == other.BarCount + && RetCode == other.RetCode; + } + + /// + /// Determines whether this series equals the given object. + /// + /// The object to compare with. + /// + /// true when is an equal to this + /// one under ; otherwise false. + /// + public override bool Equals(object? obj) + { + return obj is IndicatorSeries other && Equals(other); + } + + /// + /// Returns a hash code consistent with . + /// + /// + /// A hash code derived from the identity of the backing array together with the first bar, + /// value count, bar count and return code. + /// + public override int GetHashCode() + { + return HashCode.Combine( + RuntimeHelpers.GetHashCode(_values), + _firstBar, + WarmCount, + BarCount, + (int)RetCode); + } + + /// + /// Validates raw TA-Lib metadata and builds the series, optionally copying the values out of + /// the array it was handed. + /// + /// The return code reported by the TA-Lib call. + /// TA-Lib's BegIdx. + /// TA-Lib's NBElement. + /// The output array, already checked for null. + /// The number of bars in the source price series. + /// + /// true to copy the first elements, which is what makes the + /// public entry points immune to later mutation of the caller's array. + /// + /// A series in which every position is addressed by BAR index. + /// + /// , or is negative. + /// + /// The metadata is inconsistent; see . + private static IndicatorSeries CreateCore( + RetCode retCode, + int begIdx, + int nbElement, + double[] values, + int barCount, + bool copy) + { + ArgumentOutOfRangeException.ThrowIfNegative(begIdx); + ArgumentOutOfRangeException.ThrowIfNegative(nbElement); + ArgumentOutOfRangeException.ThrowIfNegative(barCount); + + ValidateOutputLength(nbElement, values.Length); + + // A successful call over too little data reports Success with BegIdx == 0 and NBElement == 0. + // BegIdx is meaningless in that state, so it is deliberately not examined. + if (nbElement == 0) + { + return new IndicatorSeries(retCode, 0, 0, null, barCount); + } + + ValidateFitsInSeries(begIdx, nbElement, barCount); + + double[] owned = copy ? values[..nbElement] : values; + + return new IndicatorSeries(retCode, begIdx, nbElement, owned, barCount); + } + + /// + /// Throws when a result claims more values than its output array can hold. + /// + /// The number of values the result claims. + /// The length of the output array. + /// The claim exceeds the array. + private static void ValidateOutputLength(int nbElement, int valuesLength) + { + if (nbElement > valuesLength) + { + throw new ArgumentException( + string.Create( + CultureInfo.InvariantCulture, + $"The result claims {nbElement} value(s) but its output array holds only {valuesLength}."), + nameof(nbElement)); + } + } + + /// + /// Throws when the bars a result describes do not fit inside the source price series. + /// + /// The BAR index described by output array element 0. + /// The number of values the result claims. + /// The number of bars in the source price series. + /// + /// The last described bar, begIdx + nbElement - 1, lies outside the price series. The + /// result is misaligned at its source; clamping it here would hide that behind a silently + /// shifted series. + /// + private static void ValidateFitsInSeries(int begIdx, int nbElement, int barCount) + { + if ((long)begIdx + nbElement > barCount) + { + throw new ArgumentException( + string.Create( + CultureInfo.InvariantCulture, + $"The result describes bars {begIdx}..{begIdx + nbElement - 1} but the price series holds only " + + $"{barCount} bar(s). The indicator's alignment metadata is inconsistent and the values cannot be " + + $"placed on the price series."), + nameof(nbElement)); + } + } + + /// + /// Converts a BAR index into an index into the raw TA-Lib output array. + /// + /// A BAR index into the source price series. + /// + /// The corresponding ARRAY index, which is meaningful only when it lies in + /// [0, WarmCount) β€” see . It is negative for a bar before + /// the indicator warmed up. + /// + /// + /// This expression is the only bar-to-array conversion in the library. Every member that + /// needs one delegates here, so there is exactly one place where the alignment can be got + /// wrong, and it is covered by the regression suite. + /// + private int ToArrayIndex(int bar) + { + return bar - _firstBar; + } + + /// + /// Converts an index into the raw TA-Lib output array into a BAR index. + /// + /// An ARRAY index in [0, WarmCount). + /// The BAR index that array element describes. + /// + /// The inverse of , and likewise the only array-to-bar conversion in + /// the library. Used by , the projections and the enumerator. + /// + private int ToBarIndex(int arrayIndex) + { + return _firstBar + arrayIndex; + } + + /// + /// Determines whether the given ARRAY index addresses a value that TA-Lib actually wrote. + /// + /// An ARRAY index, possibly out of range or negative. + /// true when the index lies in [0, WarmCount); otherwise false. + private bool IsWarmArrayIndex(int arrayIndex) + { + return (uint)arrayIndex < (uint)WarmCount; + } + + /// + /// Returns the pair yielded by the enumerator for the given ARRAY index. + /// + /// An ARRAY index in [0, WarmCount). + /// The BAR index that element describes, together with its value. + private (int Bar, double Value) PairAt(int arrayIndex) + { + return (ToBarIndex(arrayIndex), _values![arrayIndex]); + } + + /// + /// Throws when the given BAR index is outside the bars this series covers. + /// + /// A BAR index, expected to lie in [0, BarCount). + /// + /// is negative or greater than or equal to . + /// + private void ValidateBar(int bar) + { + if ((uint)bar >= (uint)BarCount) + { + throw new ArgumentOutOfRangeException( + nameof(bar), + bar, + string.Create( + CultureInfo.InvariantCulture, + $"The bar index must lie in [0, {BarCount}). A bar inside that range that has no value is reported " + + $"as null rather than as an exception.")); + } + } + + /// + /// Throws when the given series does not cover the same bars as this one. + /// + /// The series being crossed against this one. + /// + /// covers a different number of bars. + /// + private void ValidateComparable(IndicatorSeries other) + { + if (other.BarCount != BarCount) + { + throw new ArgumentException( + string.Create( + CultureInfo.InvariantCulture, + $"The other series covers {other.BarCount} bar(s) but this one covers {BarCount}. Two series can " + + $"only be crossed when a bar index means the same thing in both."), + nameof(other)); + } + } + + /// + /// Writes the bar-aligned projection into the first elements of a span. + /// + /// + /// A span of at least elements; only that prefix is written. + /// + private void WriteBarAligned(Span destination) + { + Span bars = destination[..BarCount]; + bars.Fill(double.NaN); + + if (WarmCount > 0) + { + WarmValues.CopyTo(bars.Slice(ToBarIndex(0), WarmCount)); + } + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/Indicators/MomentumIndicators.cs b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/MomentumIndicators.cs new file mode 100644 index 00000000..13975711 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/MomentumIndicators.cs @@ -0,0 +1,208 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// Fluent momentum indicators. +/// +/// +/// +/// The class name is TA-Lib's own function group and never appears at a call site: these are +/// extension methods on . +/// +/// +/// No indicator here returns a thresholded verdict such as "overbought". A wrong number looks +/// wrong; a wrong looks authoritative and has already destroyed the evidence +/// that would have shown it was wrong. The conventional levels are also conventions β€” 70 and 30 +/// come from a 1978 book about daily bars β€” so they belong in your source file, under your review, +/// not in the library. +/// +/// +public static class MomentumIndicators +{ + /// + /// Computes the relative strength index of the closing prices. + /// + /// The price series. + /// The lookback period. Defaults to 14, as in the raw layer. + /// + /// A bar-aligned series of values from 0 to 100. A strictly rising series is pinned at 100 and a + /// flat series reads 0, matching the reference C implementation. If the series holds fewer bars + /// than the period needs, the result is empty and reports success β€” and an empty result answers + /// null rather than 0, so it can never be mistaken for an oversold reading. + /// + /// + /// is outside 2 to 100000. + /// + public static IndicatorSeries Rsi(this PriceSeries prices, int timePeriod = 14) + { + HighLevelGuard.Period(timePeriod, nameof(timePeriod)); + + if (prices.IsEmpty) + { + return IndicatorSeries.Empty(0); + } + + RsiResult result = TAMath.Rsi(0, prices.BarCount - 1, prices.CloseArray, timePeriod); + HighLevelGuard.Succeeded(result.RetCode, nameof(Rsi)); + + return prices.AlignOwning(result, static r => r.Real); + } + + /// + /// Computes the average directional index β€” the strength of a trend, without its direction. + /// + /// + /// The price series. It must carry high and low prices, because the directional movement of a + /// bar is defined by how its range extends beyond the previous bar's. + /// + /// The Wilder smoothing period. Defaults to 14, as in the raw layer. + /// + /// A bar-aligned series of values from 0 to 100. The first bar is + /// (2 * timePeriod) + unstablePeriod - 1, which is 27 for the default period β€” the + /// longest warm-up of any indicator on this surface, because the index is a smoothed average of + /// a smoothed average. A value says only how strongly price is trending; whether it is trending + /// up or down is what TAMath.PlusDI and TAMath.MinusDI answer. + /// + /// + /// is outside 2 to 100000. + /// + /// + /// The price series carries no high and low prices. + /// + public static IndicatorSeries Adx(this PriceSeries prices, int timePeriod = 14) + { + HighLevelGuard.Period(timePeriod, nameof(timePeriod)); + HighLevelGuard.RequireHighLow(prices, nameof(Adx)); + + if (prices.IsEmpty) + { + return IndicatorSeries.Empty(0); + } + + AdxResult result = TAMath.Adx( + 0, + prices.BarCount - 1, + prices.HighArray, + prices.LowArray, + prices.CloseArray, + timePeriod); + + HighLevelGuard.Succeeded(result.RetCode, nameof(Adx)); + + return prices.AlignOwning(result, static r => r.Real); + } + + /// + /// Computes the moving average convergence divergence of the closing prices. + /// + /// The price series. + /// The fast exponential moving average period. Defaults to 12, as in the raw layer. + /// The slow exponential moving average period. Defaults to 26, as in the raw layer. + /// + /// The period of the signal line, an exponential moving average of the MACD line. Defaults to 9, + /// as in the raw layer. + /// + /// + /// The line, signal and histogram, each independently bar-aligned and sharing the same first + /// bar. A signal crossing is macd.Line.CrossedAbove(macd.Signal, bar). + /// + /// + /// , or + /// is outside 2 to 100000. The signal period matters most: the + /// raw layer does not reject a signal period of 1, it fails inside an internal array copy. + /// + public static MacdSeries Macd( + this PriceSeries prices, + int fastPeriod = 12, + int slowPeriod = 26, + int signalPeriod = 9) + { + HighLevelGuard.Period(fastPeriod, nameof(fastPeriod)); + HighLevelGuard.Period(slowPeriod, nameof(slowPeriod)); + HighLevelGuard.Period(signalPeriod, nameof(signalPeriod)); + + if (prices.IsEmpty) + { + IndicatorSeries empty = IndicatorSeries.Empty(0); + return new MacdSeries(empty, empty, empty); + } + + MacdResult result = TAMath.Macd( + 0, + prices.BarCount - 1, + prices.CloseArray, + fastPeriod, + slowPeriod, + signalPeriod); + + HighLevelGuard.Succeeded(result.RetCode, nameof(Macd)); + + return new MacdSeries( + prices.AlignOwning(result, static r => r.MacdValue), + prices.AlignOwning(result, static r => r.MacdSignal), + prices.AlignOwning(result, static r => r.MacdHist)); + } + + /// + /// Computes the slow stochastic oscillator. + /// + /// + /// The price series. It must carry high and low prices, because the oscillator measures where + /// the close sits inside the recent range. + /// + /// The lookback of the raw %K. Defaults to 5, as in the raw layer. + /// The smoothing period applied to %K. Defaults to 3, as in the raw layer. + /// The moving average used to smooth %K. Defaults to simple. + /// The period of %D. Defaults to 3, as in the raw layer. + /// The moving average used for %D. Defaults to simple. + /// The %K and %D lines, each independently bar-aligned. + /// + /// , or + /// is outside 2 to 100000. + /// + /// + /// The price series carries no high and low prices. + /// + public static StochSeries Stoch( + this PriceSeries prices, + int fastKPeriod = 5, + int slowKPeriod = 3, + MAType slowKMAType = MAType.Sma, + int slowDPeriod = 3, + MAType slowDMAType = MAType.Sma) + { + HighLevelGuard.Period(fastKPeriod, nameof(fastKPeriod)); + HighLevelGuard.Period(slowKPeriod, nameof(slowKPeriod)); + HighLevelGuard.Period(slowDPeriod, nameof(slowDPeriod)); + HighLevelGuard.RequireHighLow(prices, nameof(Stoch)); + + if (prices.IsEmpty) + { + IndicatorSeries empty = IndicatorSeries.Empty(0); + return new StochSeries(empty, empty); + } + + StochResult result = TAMath.Stoch( + 0, + prices.BarCount - 1, + prices.HighArray, + prices.LowArray, + prices.CloseArray, + fastKPeriod, + slowKPeriod, + slowKMAType, + slowDPeriod, + slowDMAType); + + HighLevelGuard.Succeeded(result.RetCode, nameof(Stoch)); + + return new StochSeries( + prices.AlignOwning(result, static r => r.SlowK), + prices.AlignOwning(result, static r => r.SlowD)); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/Indicators/OverlapStudyIndicators.cs b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/OverlapStudyIndicators.cs new file mode 100644 index 00000000..dc277b41 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/OverlapStudyIndicators.cs @@ -0,0 +1,134 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// Fluent overlap study indicators β€” those plotted on the price scale. +/// +/// +/// +/// The class name is TA-Lib's own function group and never appears at a call site: these are +/// extension methods on , so they appear directly on prices. in +/// IntelliSense. +/// +/// +/// Every default here is copied verbatim from the corresponding raw entry point. The fluent layer +/// never invents a number, so that a fluent call and a raw call with the same arguments cannot +/// disagree. +/// +/// +public static class OverlapStudyIndicators +{ + /// + /// Computes the simple moving average of the closing prices. + /// + /// The price series. + /// The number of bars to average. Defaults to 30, as in the raw layer. + /// + /// A bar-aligned series whose first bar is timePeriod - 1. If the series holds fewer bars + /// than the period needs, the result is empty and reports success β€” that is not an error. + /// + /// + /// is outside 2 to 100000. + /// + public static IndicatorSeries Sma(this PriceSeries prices, int timePeriod = 30) + { + HighLevelGuard.Period(timePeriod, nameof(timePeriod)); + + if (prices.IsEmpty) + { + return IndicatorSeries.Empty(0); + } + + SmaResult result = TAMath.Sma(0, prices.BarCount - 1, prices.CloseArray, timePeriod); + HighLevelGuard.Succeeded(result.RetCode, nameof(Sma)); + + return prices.AlignOwning(result, static r => r.Real); + } + + /// + /// Computes the exponential moving average of the closing prices. + /// + /// The price series. + /// The smoothing period. Defaults to 30, as in the raw layer. + /// + /// A bar-aligned series. If the series holds fewer bars than the period needs, the result is + /// empty and reports success. + /// + /// + /// is outside 2 to 100000. + /// + public static IndicatorSeries Ema(this PriceSeries prices, int timePeriod = 30) + { + HighLevelGuard.Period(timePeriod, nameof(timePeriod)); + + if (prices.IsEmpty) + { + return IndicatorSeries.Empty(0); + } + + EmaResult result = TAMath.Ema(0, prices.BarCount - 1, prices.CloseArray, timePeriod); + HighLevelGuard.Succeeded(result.RetCode, nameof(Ema)); + + return prices.AlignOwning(result, static r => r.Real); + } + + /// + /// Computes Bollinger Bands over the closing prices. + /// + /// The price series. + /// + /// The period of the middle band and of the standard deviation. Defaults to 5, which is + /// TA-Lib's own default, even though 20 is the conventional trading choice; pass 20 + /// explicitly if that is what you want. The fluent layer never silently redefines a default. + /// + /// + /// The number of standard deviations for the upper band. Defaults to 2. Not validated: any + /// finite deviation is meaningful. + /// + /// + /// The number of standard deviations for the lower band. Defaults to 2. Not validated. + /// + /// The type of moving average used for the middle band. Defaults to simple. + /// + /// The three bands, each independently bar-aligned and sharing the same first bar. + /// + /// + /// is outside 2 to 100000. + /// + public static BollingerBandsSeries BollingerBands( + this PriceSeries prices, + int timePeriod = 5, + double nbDevUp = 2.0, + double nbDevDn = 2.0, + MAType maType = MAType.Sma) + { + HighLevelGuard.Period(timePeriod, nameof(timePeriod)); + + if (prices.IsEmpty) + { + IndicatorSeries empty = IndicatorSeries.Empty(0); + return new BollingerBandsSeries(empty, empty, empty); + } + + BollingerBandsResult result = TAMath.BollingerBands( + 0, + prices.BarCount - 1, + prices.CloseArray, + timePeriod, + nbDevUp, + nbDevDn, + maType); + + HighLevelGuard.Succeeded(result.RetCode, nameof(BollingerBands)); + + return new BollingerBandsSeries( + prices.AlignOwning(result, static r => r.RealUpperBand), + prices.AlignOwning(result, static r => r.RealMiddleBand), + prices.AlignOwning(result, static r => r.RealLowerBand)); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/Indicators/VolatilityIndicators.cs b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/VolatilityIndicators.cs new file mode 100644 index 00000000..d723e828 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/VolatilityIndicators.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// Fluent volatility indicators. +/// +/// +/// The class name is TA-Lib's own function group and never appears at a call site: these are +/// extension methods on . +/// +public static class VolatilityIndicators +{ + /// + /// Computes the average true range. + /// + /// + /// The price series. It must carry high and low prices: a true range is a property of a bar's + /// range, and the absolute change in close is not a substitute for it. + /// + /// The smoothing period. Defaults to 14, as in the raw layer. + /// + /// A bar-aligned series whose first bar is . If the series holds + /// fewer bars than the period needs, the result is empty and reports success. + /// + /// + /// is outside 2 to 100000. This is deliberately stricter than the + /// raw entry point, which accepts 1, in exchange for one period rule that holds everywhere. + /// + /// + /// The price series carries no high and low prices. + /// + public static IndicatorSeries Atr(this PriceSeries prices, int timePeriod = 14) + { + HighLevelGuard.Period(timePeriod, nameof(timePeriod)); + HighLevelGuard.RequireHighLow(prices, nameof(Atr)); + + if (prices.IsEmpty) + { + return IndicatorSeries.Empty(0); + } + + AtrResult result = TAMath.Atr( + 0, + prices.BarCount - 1, + prices.HighArray, + prices.LowArray, + prices.CloseArray, + timePeriod); + + HighLevelGuard.Succeeded(result.RetCode, nameof(Atr)); + + return prices.AlignOwning(result, static r => r.Real); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/Indicators/VolumeIndicators.cs b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/VolumeIndicators.cs new file mode 100644 index 00000000..5fb98942 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/Indicators/VolumeIndicators.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// Fluent volume indicators. +/// +/// +/// The class name is TA-Lib's own function group and never appears at a call site: these are +/// extension methods on . +/// +public static class VolumeIndicators +{ + /// + /// Computes on-balance volume: the running total of volume, signed by the direction of the + /// close. + /// + /// + /// The price series. It must carry volumes, which only + /// supplies; substituting a constant volume would turn this into a signed bar counter. + /// + /// + /// A bar-aligned series that is warm from bar 0, because the running total needs no + /// lookback. The absolute level carries no meaning β€” it depends on where the series happens to + /// start β€” so only its direction and its divergence from price are read. + /// + /// + /// The price series carries no volumes. + /// + public static IndicatorSeries Obv(this PriceSeries prices) + { + HighLevelGuard.RequireVolume(prices, nameof(Obv)); + + if (prices.IsEmpty) + { + return IndicatorSeries.Empty(0); + } + + ObvResult result = TAMath.Obv(0, prices.BarCount - 1, prices.CloseArray, prices.VolumeArray); + HighLevelGuard.Succeeded(result.RetCode, nameof(Obv)); + + return prices.AlignOwning(result, static r => r.Real); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/Internal/HighLevelGuard.cs b/src/TechnicalAnalysis.Functions/HighLevel/Internal/HighLevelGuard.cs new file mode 100644 index 00000000..8ba134c0 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/Internal/HighLevelGuard.cs @@ -0,0 +1,116 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; + +namespace TechnicalAnalysis.Functions; + +/// +/// The preconditions every fluent indicator checks, in one place. +/// +/// +/// +/// Centralising the guards is what makes the rules uniform: no indicator author can invent a +/// different minimum period or a different way of reporting missing data. Every fluent indicator +/// validates its periods first, then the price components it needs, and only then short-circuits on +/// an empty price series β€” so a bad period is reported even when there is no data to compute over. +/// +/// +/// The period rule is deliberately stricter than parts of the raw layer, where a handful of +/// entry points accept a period of 1. One rule that cannot be got wrong is worth more than +/// per-indicator fidelity to an inconsistency, and it is what stops a signal period of 1 from +/// crashing inside an array copy deep in the MACD implementation. +/// +/// +internal static class HighLevelGuard +{ + /// + /// The smallest period any fluent indicator accepts. + /// + internal const int MinPeriod = ValidationHelper.MinPeriod; + + /// + /// The largest period any fluent indicator accepts. + /// + internal const int MaxPeriod = ValidationHelper.MaxPeriod; + + /// + /// Validates an integer period parameter. + /// + /// The period supplied by the caller. + /// The name of the parameter that carried it. + /// + /// is outside to . + /// + internal static void Period(int value, string parameterName) + { + if (value is < MinPeriod or > MaxPeriod) + { + throw new ArgumentOutOfRangeException( + parameterName, + value, + string.Create( + CultureInfo.InvariantCulture, + $"The period must lie between {MinPeriod} and {MaxPeriod}.")); + } + } + + /// + /// Requires that the price series carries high and low prices. + /// + /// The price series the indicator was called on. + /// The name of the indicator, for the message. + /// The series carries no high and low prices. + internal static void RequireHighLow(in PriceSeries prices, string indicator) + { + if (!prices.HasHighLow) + { + throw new InvalidOperationException( + $"{indicator} needs the high and low of each bar, which this price series does not carry. Build it " + + $"with PriceSeries.FromHlc, FromOhlc or FromOhlcv; the closing price alone is never substituted for a " + + $"bar's range."); + } + } + + /// + /// Requires that the price series carries volumes. + /// + /// The price series the indicator was called on. + /// The name of the indicator, for the message. + /// The series carries no volumes. + internal static void RequireVolume(in PriceSeries prices, string indicator) + { + if (!prices.HasVolume) + { + throw new InvalidOperationException( + $"{indicator} needs the volume of each bar, which this price series does not carry. Build it with " + + $"PriceSeries.FromOhlcv."); + } + } + + /// + /// Requires that a raw call succeeded. + /// + /// The code the raw call returned. + /// The name of the indicator, for the message. + /// + /// Unreachable through the fluent surface, because the guards above have already rejected every + /// input the raw layer can refuse. It exists so that a failure can never degrade quietly into + /// an empty series, which would be indistinguishable from an indicator that has not warmed up. + /// + /// is not success. + internal static void Succeeded(RetCode retCode, string indicator) + { + if (retCode != Success) + { + throw new InvalidOperationException( + string.Create( + CultureInfo.InvariantCulture, + $"{indicator} failed with {retCode}. This indicates a defect in the library rather than in the " + + $"call, because the fluent layer validates every parameter before computing.")); + } + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/MacdSeries.cs b/src/TechnicalAnalysis.Functions/HighLevel/MacdSeries.cs new file mode 100644 index 00000000..34cbba3c --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/MacdSeries.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// The three bar-aligned outputs of a MACD calculation. +/// +/// +/// The MACD line: the fast exponential moving average minus the slow one. Bar-aligned. +/// +/// +/// The signal line: an exponential moving average of . Bar-aligned. +/// +/// +/// The histogram: minus . Bar-aligned. +/// +/// +/// Each component is an independently addressable , so a signal-line +/// crossing needs no dedicated member: macd.Line.CrossedAbove(macd.Signal, bar) already says +/// it, in bar indices, with the differing warm-ups handled for you. +/// +public readonly record struct MacdSeries(IndicatorSeries Line, IndicatorSeries Signal, IndicatorSeries Histogram) +{ + /// + /// Narrows every component so that it ends at the given BAR index. + /// + /// + /// The last BAR index the narrowed components are allowed to know about, with domain + /// [0, BarCount) of the components. + /// + /// A result whose three components have each been narrowed by . + /// + /// is outside the bars the components cover. + /// + public MacdSeries AsOf(int bar) + { + return new MacdSeries(Line.AsOf(bar), Signal.AsOf(bar), Histogram.AsOf(bar)); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/PriceSeries.cs b/src/TechnicalAnalysis.Functions/HighLevel/PriceSeries.cs new file mode 100644 index 00000000..26cb3bb0 --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/PriceSeries.cs @@ -0,0 +1,691 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace TechnicalAnalysis.Functions; + +/// +/// An immutable price series β€” the entry point to the fluent indicator API. +/// +/// +/// +/// Build one with the factory that matches the data you actually have, then call an indicator on +/// it. Every position on this type is a BAR index with domain [0, BarCount). +/// +/// +/// The factories copy. Immutability is unconditional rather than dependent on caller +/// discipline, so a and every derived from +/// it are safe for unrestricted concurrent use with no caveat. Because the factories take +/// , a caller with an oversized scratch buffer pays only for the valid +/// region: PriceSeries.FromHlc(high.AsSpan(0, count), low.AsSpan(0, count), close.AsSpan(0, count)). +/// +/// +/// The factories reject non-finite prices. A close β€” a gapped tick, +/// a provider sentinel, a bad CSV parse β€” is refused at the boundary with an +/// naming the component and the first offending bar. It has to be: +/// the running sums inside TA-Lib's simple moving average, and the recursions inside the +/// exponential moving average, the average true range and the relative strength index, all +/// propagate a single to every later bar. One bad tick would otherwise +/// poison the whole series while every status flag still read success, and +/// if (sma[bar] is { } value) would keep succeeding with a value that compares +/// false against every threshold. The factories already walk the data to copy it, so the +/// check costs nothing. +/// +/// +/// There is no startIdx or endIdx anywhere on this surface. Indicators always +/// analyse the full series, so the first bar of a result is always the indicator's true lookback, +/// and a caller can never trigger the unguarded end-index buffer overrun that a raw +/// TAMath call permits. Windowing is expressed by , which keeps bar +/// indices absolute instead of rebasing them. +/// +/// +/// Missing components are never fabricated. A close-only series does not pretend that open, +/// high and low equal the close; asking for them throws. Otherwise an ATR would silently compute +/// the absolute change in close and call it a true range. +/// +/// +/// There is no caching. Calling prices.Sma(20) twice computes twice. Because this is +/// a value type with no lazy state, caching is one field at the composition root when it is wanted, +/// and there is no shared mutable state and no locking when it is not. +/// +/// +/// +/// +/// PriceSeries prices = PriceSeries.FromHlc(highs, lows, closes); +/// double? rsi = prices.Rsi(14).Latest; +/// double? atr = prices.Atr(14).Latest; +/// +/// +public readonly struct PriceSeries : IEquatable +{ + /// The opening prices, or null when the series carries none. + private readonly double[]? _open; + + /// The high prices, or null when the series carries none. + private readonly double[]? _high; + + /// The low prices, or null when the series carries none. + private readonly double[]? _low; + + /// The closing prices, or null when the series is empty. + private readonly double[]? _close; + + /// The volumes, or null when the series carries none. + private readonly double[]? _volume; + + /// + /// Initializes a new instance of the struct. + /// + /// The opening prices, or null. + /// The high prices, or null. + /// The low prices, or null. + /// The closing prices, or null. + /// The volumes, or null. + /// The number of bars the series exposes, which may be fewer than the arrays hold. + private PriceSeries(double[]? open, double[]? high, double[]? low, double[]? close, double[]? volume, int barCount) + { + _open = open; + _high = high; + _low = low; + _close = close; + _volume = volume; + BarCount = barCount; + } + + /// + /// Gets the empty price series. + /// + /// + /// + /// A series of zero bars carrying no components at all. Equivalent to + /// default(PriceSeries), which is what makes it a usable pre-roll state for a field that + /// is filled in later. + /// + /// + /// It carries no high, low or volume, so the indicators that need them throw. + /// , , + /// , + /// and need only closes and return an empty result; + /// , and + /// raise because + /// component availability is checked before emptiness, and + /// raises it for the missing volumes. A feed that must + /// answer every indicator while it has no bars yet is + /// PriceSeries.FromOhlcv([], [], [], [], []) β€” the components are then present and + /// merely empty. + /// + /// + public static PriceSeries Empty => default; + + /// + /// Gets the number of bars in the series. + /// + /// + /// A count, not an index. Every BAR index on this type and on the indicator series derived from + /// it has domain [0, BarCount). + /// + public int BarCount { get; } + + /// + /// Gets a value indicating whether the series holds no bars. + /// + /// true when is zero. + public bool IsEmpty => BarCount == 0; + + /// + /// Gets a value indicating whether the series carries opening prices. + /// + /// true when the series was built by or . + public bool HasOpen => _open is not null; + + /// + /// Gets a value indicating whether the series carries high and low prices. + /// + /// + /// true when the series was built by , or + /// . Indicators that need a bar's range, such as ATR and the stochastic + /// oscillator, require this. + /// + public bool HasHighLow => _high is not null && _low is not null; + + /// + /// Gets a value indicating whether the series carries volumes. + /// + /// true when the series was built by . + public bool HasVolume => _volume is not null; + + /// + /// Gets the closing prices. + /// + /// + /// A read-only span of exactly elements indexed by BAR index; an empty + /// span when the series holds no bars. Every factory requires closes, so this never throws. + /// + public ReadOnlySpan Close => Span(_close); + + /// + /// Gets the opening prices. + /// + /// A read-only span of exactly elements indexed by BAR index. + /// + /// The series carries no opening prices; see . + /// + public ReadOnlySpan Open + { + get + { + RequireComponent(HasOpen, "Open prices", nameof(FromOhlc)); + return Span(_open); + } + } + + /// + /// Gets the high prices. + /// + /// A read-only span of exactly elements indexed by BAR index. + /// + /// The series carries no high prices; see . + /// + public ReadOnlySpan High + { + get + { + RequireComponent(HasHighLow, "High prices", nameof(FromHlc)); + return Span(_high); + } + } + + /// + /// Gets the low prices. + /// + /// A read-only span of exactly elements indexed by BAR index. + /// + /// The series carries no low prices; see . + /// + public ReadOnlySpan Low + { + get + { + RequireComponent(HasHighLow, "Low prices", nameof(FromHlc)); + return Span(_low); + } + } + + /// + /// Gets the volumes. + /// + /// A read-only span of exactly elements indexed by BAR index. + /// + /// The series carries no volumes; see . + /// + public ReadOnlySpan Volume + { + get + { + RequireComponent(HasVolume, "Volumes", nameof(FromOhlcv)); + return Span(_volume); + } + } + + /// + /// Gets the backing array of closing prices for the indicator extensions in this assembly. + /// + /// + /// The full backing array, which may be longer than after + /// . Never null. Callers must pass BarCount - 1 as the end index + /// so that only the bars this series exposes are analysed. + /// + internal double[] CloseArray => _close ?? []; + + /// + /// Gets the backing array of high prices for the indicator extensions in this assembly. + /// + /// The full backing array, or an empty array when the component is absent. + internal double[] HighArray => _high ?? []; + + /// + /// Gets the backing array of low prices for the indicator extensions in this assembly. + /// + /// The full backing array, or an empty array when the component is absent. + internal double[] LowArray => _low ?? []; + + /// + /// Gets the backing array of volumes for the indicator extensions in this assembly. + /// + /// The full backing array, or an empty array when the component is absent. + internal double[] VolumeArray => _volume ?? []; + + /// + /// Creates a price series from closing prices alone. + /// + /// The closing prices, copied into the new series. May be empty. + /// A series with equal to the length of . + /// + /// The resulting series carries no open, high, low or volume, and does not fabricate them. + /// Indicators that need a bar's range throw rather than silently computing something else. + /// + /// + /// holds a value that is not finite. + /// + public static PriceSeries FromClose(ReadOnlySpan close) + { + ValidateFinite(close, nameof(close)); + + return new PriceSeries(null, null, null, close.ToArray(), null, close.Length); + } + + /// + /// Creates a price series from high, low and closing prices. + /// + /// The high prices. Must be the same length as . + /// The low prices. Must be the same length as . + /// The closing prices, which define the number of bars. May be empty. + /// A series with equal to the length of . + /// + /// or has a different length from + /// , or any component holds a value that is not finite. + /// + public static PriceSeries FromHlc(ReadOnlySpan high, ReadOnlySpan low, ReadOnlySpan close) + { + int barCount = close.Length; + ValidateLength(high, barCount, nameof(high)); + ValidateLength(low, barCount, nameof(low)); + ValidateFinite(high, nameof(high)); + ValidateFinite(low, nameof(low)); + ValidateFinite(close, nameof(close)); + + return new PriceSeries(null, high.ToArray(), low.ToArray(), close.ToArray(), null, barCount); + } + + /// + /// Creates a price series from open, high, low and closing prices. + /// + /// The opening prices. Must be the same length as . + /// The high prices. Must be the same length as . + /// The low prices. Must be the same length as . + /// The closing prices, which define the number of bars. May be empty. + /// A series with equal to the length of . + /// + /// , or has a different + /// length from , or any component holds a value that is not finite. + /// Components are checked in the order open, high, low. + /// + public static PriceSeries FromOhlc( + ReadOnlySpan open, + ReadOnlySpan high, + ReadOnlySpan low, + ReadOnlySpan close) + { + int barCount = close.Length; + ValidateLength(open, barCount, nameof(open)); + ValidateLength(high, barCount, nameof(high)); + ValidateLength(low, barCount, nameof(low)); + ValidateFinite(open, nameof(open)); + ValidateFinite(high, nameof(high)); + ValidateFinite(low, nameof(low)); + ValidateFinite(close, nameof(close)); + + return new PriceSeries(open.ToArray(), high.ToArray(), low.ToArray(), close.ToArray(), null, barCount); + } + + /// + /// Creates a price series from open, high, low and closing prices together with volumes. + /// + /// The opening prices. Must be the same length as . + /// The high prices. Must be the same length as . + /// The low prices. Must be the same length as . + /// The closing prices, which define the number of bars. May be empty. + /// The volumes. Must be the same length as . + /// A series with equal to the length of . + /// + /// , , or + /// has a different length from , or any + /// component holds a value that is not finite. Components are checked in the order open, high, + /// low, volume. + /// + public static PriceSeries FromOhlcv( + ReadOnlySpan open, + ReadOnlySpan high, + ReadOnlySpan low, + ReadOnlySpan close, + ReadOnlySpan volume) + { + int barCount = close.Length; + ValidateLength(open, barCount, nameof(open)); + ValidateLength(high, barCount, nameof(high)); + ValidateLength(low, barCount, nameof(low)); + ValidateLength(volume, barCount, nameof(volume)); + ValidateFinite(open, nameof(open)); + ValidateFinite(high, nameof(high)); + ValidateFinite(low, nameof(low)); + ValidateFinite(close, nameof(close)); + ValidateFinite(volume, nameof(volume)); + + return new PriceSeries( + open.ToArray(), + high.ToArray(), + low.ToArray(), + close.ToArray(), + volume.ToArray(), + barCount); + } + + /// + /// Determines whether two price series are equal. + /// + /// The first series. + /// The second series. + /// true when the two series are equal; otherwise false. + public static bool operator ==(PriceSeries left, PriceSeries right) + { + return left.Equals(right); + } + + /// + /// Determines whether two price series are not equal. + /// + /// The first series. + /// The second series. + /// true when the two series are not equal; otherwise false. + public static bool operator !=(PriceSeries left, PriceSeries right) + { + return !left.Equals(right); + } + + /// + /// Returns the same price series truncated so that it ends at the given BAR index. + /// + /// + /// The last BAR index the narrowed series is allowed to know about, with domain + /// [0, BarCount). + /// + /// + /// A series with equal to bar + 1, sharing the same underlying + /// data. Every span truncates accordingly, and bar indices are not rebased. + /// + /// + /// This is how a backtest hands a strategy the prices without handing it the future: the later + /// bars are not part of the value at all, so no discipline is required to avoid reading them. + /// It is allocation-free, which is what makes it usable as the default idiom inside a per-bar + /// loop. + /// + /// + /// is negative or greater than or equal to . + /// + public PriceSeries AsOf(int bar) + { + ValidateBar(bar); + + return new PriceSeries(_open, _high, _low, _close, _volume, bar + 1); + } + + /// + /// Bar-aligns the result of any single-output TAMath call made over this price series. + /// + /// + /// The raw result. It must have been computed over this series with a start index of 0 + /// and an end index of BarCount - 1, otherwise its alignment metadata does not describe + /// these bars. + /// + /// An addressed by BAR index. + /// + /// + /// This is the escape hatch to the roughly eighty single-output indicators that have no fluent + /// wrapper yet, and it is the same primitive the shipped wrappers use, so an indicator reached + /// this way is aligned exactly as carefully as one that ships. The bar count is supplied by this + /// series, so it can never be mismatched. + /// + /// + /// The values are copied. result.Real stays the caller's array and may be + /// post-processed in place afterwards without disturbing the series handed back, which is what + /// makes unconditionally immutable rather than immutable by + /// convention. The copy is O(NBElement) against an O(n) indicator computation. + /// + /// + /// Naming your own extension methods. The fluent indicators are extension methods on + /// this type declared in this namespace, and the set of them will grow towards the full TA-Lib + /// surface. A user-authored public static IndicatorSeries Cci(this PriceSeries, int) + /// therefore becomes ambiguous (CS0121) the day the library ships its own Cci. Give your + /// own extensions names the library will never take β€” a prefix such as MyCci, or a + /// receiver type of your own β€” and treat the arrival of new indicators as potentially + /// source-breaking for code that does otherwise. + /// + /// + /// is null. + /// + /// The result's alignment metadata is inconsistent with this series β€” its + /// BegIdx + NBElement does not fit inside , or its + /// NBElement exceeds its own output array. That is a defect in the indicator rather than + /// in this call, and it is surfaced rather than clamped because clamping would hand back a + /// silently shifted series. The parameter named by the exception is + /// . + /// + public IndicatorSeries Align(SingleOutputResult result) + { + ArgumentNullException.ThrowIfNull(result); + + return Align(result, static r => r.Real); + } + + /// + /// Bar-aligns one output of any multi-output TAMath call made over this price series. + /// + /// The concrete result type. + /// + /// The raw result. It must have been computed over this series with a start index of 0 + /// and an end index of BarCount - 1. + /// + /// + /// Selects the output array to align, from the same result whose metadata is used. Taking a + /// selector rather than a separate array is what prevents one result's metadata being paired + /// with another result's values, which would be a brand new way to misalign a series. + /// + /// An addressed by BAR index. + /// + /// The selected values are copied, so the result's own array remains the caller's to mutate. + /// The naming guidance on applies here too. + /// + /// + /// or is null. + /// + /// + /// The result's alignment metadata is inconsistent with this series; see + /// . The parameter named by the exception is + /// . + /// + public IndicatorSeries Align(TResult result, Func output) + where TResult : IndicatorResult + { + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(output); + + try + { + return IndicatorSeries.Create(result.RetCode, result.BegIdx, result.NBElement, output(result), BarCount); + } + catch (ArgumentException inconsistent) when (inconsistent is not ArgumentNullException) + { + // Create blames its own parameters -- nbElement, values -- which name nothing the + // caller of Align passed, and which read as "your argument is bad" when the fault is + // the indicator's metadata. Re-blame the argument the caller can actually see. + throw new ArgumentException( + $"The indicator result cannot be placed on this price series. {inconsistent.Message}", + nameof(result), + inconsistent); + } + } + + /// + /// Bar-aligns one output of a TAMath call whose output array this assembly owns. + /// + /// The concrete result type. + /// The raw result, computed over this series from bar 0 to BarCount - 1. + /// Selects the output array to align. + /// An addressed by BAR index. + /// + /// The shipped fluent indicators call this rather than the public Align: they made + /// the TAMath call themselves, the result never escapes, and so the output array is + /// reachable from nothing else and need not be copied. It performs exactly the same validation. + /// + internal IndicatorSeries AlignOwning(TResult result, Func output) + where TResult : IndicatorResult + { + return IndicatorSeries.CreateOwning(result.RetCode, result.BegIdx, result.NBElement, output(result), BarCount); + } + + /// + /// Determines whether this price series equals another. + /// + /// The series to compare with. + /// + /// true when both series share every component array by reference and expose the + /// same number of bars. + /// + /// + /// This does not compare prices. Two series built from identical inputs are not equal, because + /// the factories copy and therefore hold different arrays. Equality exists so that this value + /// type satisfies CA1815. + /// + public bool Equals(PriceSeries other) + { + return ReferenceEquals(_open, other._open) + && ReferenceEquals(_high, other._high) + && ReferenceEquals(_low, other._low) + && ReferenceEquals(_close, other._close) + && ReferenceEquals(_volume, other._volume) + && BarCount == other.BarCount; + } + + /// + /// Determines whether this price series equals the given object. + /// + /// The object to compare with. + /// + /// true when is a equal to this one + /// under ; otherwise false. + /// + public override bool Equals(object? obj) + { + return obj is PriceSeries other && Equals(other); + } + + /// + /// Returns a hash code consistent with . + /// + /// A hash code derived from the identity of the component arrays and the bar count. + public override int GetHashCode() + { + return HashCode.Combine( + RuntimeHelpers.GetHashCode(_open), + RuntimeHelpers.GetHashCode(_high), + RuntimeHelpers.GetHashCode(_low), + RuntimeHelpers.GetHashCode(_close), + RuntimeHelpers.GetHashCode(_volume), + BarCount); + } + + /// + /// Throws when a component span does not have the expected length. + /// + /// The component being validated. + /// The number of bars the closing prices define. + /// The name of the parameter carrying . + /// + /// has a different length from the closing prices. + /// + private static void ValidateLength(ReadOnlySpan component, int expected, string parameterName) + { + if (component.Length != expected) + { + throw new ArgumentException( + string.Create( + CultureInfo.InvariantCulture, + $"'{parameterName}' holds {component.Length} bar(s) but 'close' holds {expected}. Every component " + + $"of a price series must describe the same bars."), + parameterName); + } + } + + /// + /// Throws when a component holds a value that is not finite. + /// + /// The component being validated. + /// The name of the parameter carrying . + /// + /// One or infinity does not spoil only the bars it touches. TA-Lib's + /// simple moving average keeps a running sum, and NaN - finite is NaN for ever + /// after; the exponential moving average, the average true range and the relative strength + /// index are recursive and behave the same way. Refusing at the boundary converts a silent + /// whole-series corruption into one diagnosable exception that names the first bad bar. + /// + /// + /// holds a value that is not finite. + /// + private static void ValidateFinite(ReadOnlySpan component, string parameterName) + { + for (int bar = 0; bar < component.Length; bar++) + { + if (!double.IsFinite(component[bar])) + { + throw new ArgumentException( + string.Create( + CultureInfo.InvariantCulture, + $"'{parameterName}' holds {component[bar]} at bar {bar}. A price series must be finite " + + $"throughout: a single non-finite bar propagates through every running sum and every " + + $"recursion in TA-Lib and silently corrupts the whole indicator. Clean or drop the bar " + + $"before building the series."), + parameterName); + } + } + } + + /// + /// Throws when a component the caller asked for is absent. + /// + /// Whether the component is present. + /// A human-readable name for the component. + /// The name of the factory that would have supplied it. + /// The component is absent. + private static void RequireComponent(bool available, string component, string factory) + { + if (!available) + { + throw new InvalidOperationException( + $"{component} are not available on this price series. Build it with PriceSeries.{factory} " + + $"instead; a close-only series never fabricates the missing components."); + } + } + + /// + /// Throws when the given BAR index is outside the bars this series covers. + /// + /// A BAR index, expected to lie in [0, BarCount). + /// + /// is negative or greater than or equal to . + /// + private void ValidateBar(int bar) + { + if ((uint)bar >= (uint)BarCount) + { + throw new ArgumentOutOfRangeException( + nameof(bar), + bar, + string.Create(CultureInfo.InvariantCulture, $"The bar index must lie in [0, {BarCount}).")); + } + } + + /// + /// Returns the first elements of a component array. + /// + /// The component array, which may be longer than after . + /// A read-only span of exactly elements, indexed by BAR index. + private ReadOnlySpan Span(double[]? component) + { + return component is null ? default : new ReadOnlySpan(component, 0, BarCount); + } +} diff --git a/src/TechnicalAnalysis.Functions/HighLevel/StochSeries.cs b/src/TechnicalAnalysis.Functions/HighLevel/StochSeries.cs new file mode 100644 index 00000000..8233491e --- /dev/null +++ b/src/TechnicalAnalysis.Functions/HighLevel/StochSeries.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions; + +/// +/// The two bar-aligned outputs of a stochastic oscillator calculation. +/// +/// +/// The slow %K line: the smoothed position of the close within the recent high-low range, +/// expressed from 0 to 100. Bar-aligned. +/// +/// +/// The slow %D line: a moving average of , acting as its signal line. +/// Bar-aligned. +/// +/// +/// The component names match , so moving between the raw and fluent layers +/// costs nothing. A %K/%D crossing is stoch.SlowK.CrossedAbove(stoch.SlowD, bar). +/// +public readonly record struct StochSeries(IndicatorSeries SlowK, IndicatorSeries SlowD) +{ + /// + /// Narrows both lines so that they end at the given BAR index. + /// + /// + /// The last BAR index the narrowed lines are allowed to know about, with domain + /// [0, BarCount) of the lines. + /// + /// A result whose two lines have each been narrowed by . + /// + /// is outside the bars the lines cover. + /// + public StochSeries AsOf(int bar) + { + return new StochSeries(SlowK.AsOf(bar), SlowD.AsOf(bar)); + } +} diff --git a/src/TechnicalAnalysis.Functions/MinusDI/TAFunc.cs b/src/TechnicalAnalysis.Functions/MinusDI/TAFunc.cs index 7e8ea589..e2d8a07c 100644 --- a/src/TechnicalAnalysis.Functions/MinusDI/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/MinusDI/TAFunc.cs @@ -95,102 +95,91 @@ public static RetCode MinusDI( double prevHigh = inHigh[today]; double prevLow = inLow[today]; double prevClose = inClose[today]; + double tempReal; + double tempReal2; + double diffM; + double diffP; + + // The reference implementation seeds with `while (i-- > 0)` and then smooths with + // `while (i-- != 0)`; each executes its body exactly `i` times. Translating either as + // `while (true) { i--; if (i <= 0) break; ... }` executes it `i - 1` times, which leaves + // `today` short of startIdx and makes the tail loop emit that many extra values, so that + // outBegIdx + outNBElement overruns the input. int i = optInTimePeriod - 1; - while (true) + while (i > 0) { i--; - double tempReal; - double tempReal2; - double diffM; - double diffP; - if (i <= 0) + today++; + tempReal = inHigh[today]; + diffP = tempReal - prevHigh; + prevHigh = tempReal; + tempReal = inLow[today]; + diffM = prevLow - tempReal; + prevLow = tempReal; + if (diffM > 0.0 && diffP < diffM) { - i = (int)TACore.Globals.UnstablePeriod[FuncUnstId.MinusDI] + 1; - while (true) - { - i--; - if (i == 0) - { - break; - } - - today++; - tempReal = inHigh[today]; - diffP = tempReal - prevHigh; - prevHigh = tempReal; - tempReal = inLow[today]; - diffM = prevLow - tempReal; - prevLow = tempReal; - if (diffM > 0.0 && diffP < diffM) - { - prevMinusDM = prevMinusDM - (prevMinusDM / optInTimePeriod) + diffM; - } - else - { - prevMinusDM -= prevMinusDM / optInTimePeriod; - } - - tempReal = prevHigh - prevLow; - tempReal2 = Math.Abs(prevHigh - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } + prevMinusDM += diffM; + } - tempReal2 = Math.Abs(prevLow - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } + tempReal = prevHigh - prevLow; + tempReal2 = Math.Abs(prevHigh - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } - prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; - prevClose = inClose[today]; - } + tempReal2 = Math.Abs(prevLow - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } - outReal[0] = 100.0 * (prevMinusDM / prevTR); + prevTR += tempReal; + prevClose = inClose[today]; + } - outIdx = 1; - while (today < endIdx) - { - today++; - tempReal = inHigh[today]; - diffP = tempReal - prevHigh; - prevHigh = tempReal; - tempReal = inLow[today]; - diffM = prevLow - tempReal; - prevLow = tempReal; - if (diffM > 0.0 && diffP < diffM) - { - prevMinusDM = prevMinusDM - (prevMinusDM / optInTimePeriod) + diffM; - } - else - { - prevMinusDM -= prevMinusDM / optInTimePeriod; - } + i = (int)TACore.Globals.UnstablePeriod[FuncUnstId.MinusDI] + 1; + while (i != 0) + { + i--; + today++; + tempReal = inHigh[today]; + diffP = tempReal - prevHigh; + prevHigh = tempReal; + tempReal = inLow[today]; + diffM = prevLow - tempReal; + prevLow = tempReal; + if (diffM > 0.0 && diffP < diffM) + { + prevMinusDM = prevMinusDM - (prevMinusDM / optInTimePeriod) + diffM; + } + else + { + prevMinusDM -= prevMinusDM / optInTimePeriod; + } - tempReal = prevHigh - prevLow; - tempReal2 = Math.Abs(prevHigh - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } + tempReal = prevHigh - prevLow; + tempReal2 = Math.Abs(prevHigh - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } - tempReal2 = Math.Abs(prevLow - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } + tempReal2 = Math.Abs(prevLow - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } - prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; - prevClose = inClose[today]; - outReal[outIdx] = 100.0 * (prevMinusDM / prevTR); - outIdx++; - } + prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; + prevClose = inClose[today]; + } - outNBElement = outIdx; - return Success; - } + outReal[0] = 100.0 * (prevMinusDM / prevTR); + outIdx = 1; + while (today < endIdx) + { today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; @@ -200,7 +189,11 @@ public static RetCode MinusDI( prevLow = tempReal; if (diffM > 0.0 && diffP < diffM) { - prevMinusDM += diffM; + prevMinusDM = prevMinusDM - (prevMinusDM / optInTimePeriod) + diffM; + } + else + { + prevMinusDM -= prevMinusDM / optInTimePeriod; } tempReal = prevHigh - prevLow; @@ -216,9 +209,14 @@ public static RetCode MinusDI( tempReal = tempReal2; } - prevTR += tempReal; + prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; prevClose = inClose[today]; + outReal[outIdx] = 100.0 * (prevMinusDM / prevTR); + outIdx++; } + + outNBElement = outIdx; + return Success; } outBegIdx = startIdx; diff --git a/src/TechnicalAnalysis.Functions/PlusDI/TAFunc.cs b/src/TechnicalAnalysis.Functions/PlusDI/TAFunc.cs index a03d0ed8..8543488d 100644 --- a/src/TechnicalAnalysis.Functions/PlusDI/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/PlusDI/TAFunc.cs @@ -95,102 +95,53 @@ public static RetCode PlusDI( double prevHigh = inHigh[today]; double prevLow = inLow[today]; double prevClose = inClose[today]; + double tempReal; + double tempReal2; + double diffP; + double diffM; + + // The reference implementation seeds with `while (i-- > 0)` and then smooths with + // `while (i-- != 0)`; each executes its body exactly `i` times. Translating either as + // `while (true) { i--; if (i <= 0) break; ... }` executes it `i - 1` times, which leaves + // `today` short of startIdx and makes the tail loop emit that many extra values, so that + // outBegIdx + outNBElement overruns the input. int i = optInTimePeriod - 1; - while (true) + while (i > 0) { i--; - double tempReal; - double tempReal2; - double diffP; - double diffM; - if (i <= 0) + today++; + tempReal = inHigh[today]; + diffP = tempReal - prevHigh; + prevHigh = tempReal; + tempReal = inLow[today]; + diffM = prevLow - tempReal; + prevLow = tempReal; + if (diffP > 0.0 && diffP > diffM) { - i = (int)TACore.Globals.UnstablePeriod[FuncUnstId.PlusDI] + 1; - while (true) - { - i--; - if (i == 0) - { - break; - } - - today++; - tempReal = inHigh[today]; - diffP = tempReal - prevHigh; - prevHigh = tempReal; - tempReal = inLow[today]; - diffM = prevLow - tempReal; - prevLow = tempReal; - if (diffP > 0.0 && diffP > diffM) - { - prevPlusDM = prevPlusDM - (prevPlusDM / optInTimePeriod) + diffP; - } - else - { - prevPlusDM -= prevPlusDM / optInTimePeriod; - } - - tempReal = prevHigh - prevLow; - tempReal2 = Math.Abs(prevHigh - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } - - tempReal2 = Math.Abs(prevLow - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } - - prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; - prevClose = inClose[today]; - } - - outReal[0] = 100.0 * (prevPlusDM / prevTR); - - outIdx = 1; - while (today < endIdx) - { - today++; - tempReal = inHigh[today]; - diffP = tempReal - prevHigh; - prevHigh = tempReal; - tempReal = inLow[today]; - diffM = prevLow - tempReal; - prevLow = tempReal; - if (diffP > 0.0 && diffP > diffM) - { - prevPlusDM = prevPlusDM - (prevPlusDM / optInTimePeriod) + diffP; - } - else - { - prevPlusDM -= prevPlusDM / optInTimePeriod; - } - - tempReal = prevHigh - prevLow; - tempReal2 = Math.Abs(prevHigh - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } - - tempReal2 = Math.Abs(prevLow - prevClose); - if (tempReal2 > tempReal) - { - tempReal = tempReal2; - } - - prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; - prevClose = inClose[today]; - outReal[outIdx] = 100.0 * (prevPlusDM / prevTR); - outIdx++; - } - - outNBElement = outIdx; - return Success; + prevPlusDM += diffP; } + tempReal = prevHigh - prevLow; + tempReal2 = Math.Abs(prevHigh - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } + + tempReal2 = Math.Abs(prevLow - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } + + prevTR += tempReal; + prevClose = inClose[today]; + } + + i = (int)TACore.Globals.UnstablePeriod[FuncUnstId.PlusDI] + 1; + while (i != 0) + { + i--; today++; tempReal = inHigh[today]; diffP = tempReal - prevHigh; @@ -200,7 +151,11 @@ public static RetCode PlusDI( prevLow = tempReal; if (diffP > 0.0 && diffP > diffM) { - prevPlusDM += diffP; + prevPlusDM = prevPlusDM - (prevPlusDM / optInTimePeriod) + diffP; + } + else + { + prevPlusDM -= prevPlusDM / optInTimePeriod; } tempReal = prevHigh - prevLow; @@ -216,9 +171,52 @@ public static RetCode PlusDI( tempReal = tempReal2; } - prevTR += tempReal; + prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; prevClose = inClose[today]; } + + outReal[0] = 100.0 * (prevPlusDM / prevTR); + + outIdx = 1; + while (today < endIdx) + { + today++; + tempReal = inHigh[today]; + diffP = tempReal - prevHigh; + prevHigh = tempReal; + tempReal = inLow[today]; + diffM = prevLow - tempReal; + prevLow = tempReal; + if (diffP > 0.0 && diffP > diffM) + { + prevPlusDM = prevPlusDM - (prevPlusDM / optInTimePeriod) + diffP; + } + else + { + prevPlusDM -= prevPlusDM / optInTimePeriod; + } + + tempReal = prevHigh - prevLow; + tempReal2 = Math.Abs(prevHigh - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } + + tempReal2 = Math.Abs(prevLow - prevClose); + if (tempReal2 > tempReal) + { + tempReal = tempReal2; + } + + prevTR = prevTR - (prevTR / optInTimePeriod) + tempReal; + prevClose = inClose[today]; + outReal[outIdx] = 100.0 * (prevPlusDM / prevTR); + outIdx++; + } + + outNBElement = outIdx; + return Success; } outBegIdx = startIdx; diff --git a/src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs b/src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs index dbb35a7a..3a51bf08 100644 --- a/src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs @@ -93,7 +93,7 @@ public static RetCode Rsi( tempValue1 = prevLoss / optInTimePeriod; tempValue2 = prevGain / optInTimePeriod; tempValue1 = tempValue2 + tempValue1; - outReal[outIdx] = 100.0 * (tempValue2 / tempValue1); + outReal[outIdx] = RsiFromGainAndLoss(tempValue2, tempValue1); outIdx++; if (today > endIdx) @@ -131,7 +131,7 @@ public static RetCode Rsi( if (today > startIdx) { tempValue1 = prevGain + prevLoss; - outReal[outIdx] = 100.0 * (prevGain / tempValue1); + outReal[outIdx] = RsiFromGainAndLoss(prevGain, tempValue1); outIdx++; } else @@ -178,7 +178,7 @@ public static RetCode Rsi( prevLoss /= optInTimePeriod; prevGain /= optInTimePeriod; tempValue1 = prevGain + prevLoss; - outReal[outIdx] = 100.0 * (prevGain / tempValue1); + outReal[outIdx] = RsiFromGainAndLoss(prevGain, tempValue1); outIdx++; } @@ -189,6 +189,26 @@ public static RetCode Rsi( return Success; } + /// + /// Converts a smoothed average gain and the sum of the smoothed average gain and loss into an RSI reading. + /// + /// The smoothed average gain over the period. + /// The sum of the smoothed average gain and the smoothed average loss. + /// + /// 100 * averageGain / gainPlusLoss, or 0 when the window contains no price movement at all. + /// + /// + /// A window in which every close is identical produces an average gain and an average loss of exactly zero, + /// so the unguarded ratio is 0 / 0 β€” , returned alongside + /// and a non-zero element count, which is indistinguishable from a real + /// reading until it poisons whatever consumes it. A halted instrument or any flat window reaches this. + /// TA-Lib C guards the same division and yields zero, so this matches upstream behaviour. + /// + private static double RsiFromGainAndLoss(double averageGain, double gainPlusLoss) + { + return gainPlusLoss > 0.0 ? 100.0 * (averageGain / gainPlusLoss) : 0.0; + } + /// /// Calculates the lookback period for the RSI indicator. /// diff --git a/src/TechnicalAnalysis.Functions/TAFunc.cs b/src/TechnicalAnalysis.Functions/TAFunc.cs index 52e6d60e..38b749bd 100644 --- a/src/TechnicalAnalysis.Functions/TAFunc.cs +++ b/src/TechnicalAnalysis.Functions/TAFunc.cs @@ -81,18 +81,19 @@ private static RetCode TA_INT_EMA(int startIdx, int endIdx, double[] inReal0, in today = startIdx - lookbackTotal; int i = optInTimePeriod0; double tempReal = 0.0; - while (true) + + // Seeds the EMA with the simple average of the first optInTimePeriod0 values, matching + // TA-Lib C's `while (i-- > 0)`. Testing the counter before decrementing it (rather than + // after) is what keeps the final term in the sum: the earlier `i--; if (i <= 0) break;` + // form accumulated only optInTimePeriod0 - 1 values while still dividing by + // optInTimePeriod0, seeding every EMA low by a factor of (period - 1) / period. + while (i > 0) { i--; - - if (i <= 0) - { - break; - } - tempReal += inReal0[today]; today++; } + prevMA = tempReal / optInTimePeriod0; } diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/Func/AtrTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/Func/AtrTests.cs index 4600e7b9..8e2ddd13 100644 --- a/tests/TechnicalAnalysis.Functions.UnitTests/Func/AtrTests.cs +++ b/tests/TechnicalAnalysis.Functions.UnitTests/Func/AtrTests.cs @@ -55,4 +55,67 @@ public void AtrFloat() actualResult.ShouldNotBeNull(); actualResult.RetCode.ShouldBe(RetCode.Success); } + + [Fact] + public void AtrOnConstantTrueRangeEqualsThatTrueRange() + { + // Arrange + // Every bar is identical, so the true range is max(104 - 100, |104 - 102|, |100 - 102|) = 4 + // on every bar. Wilder's average of a constant is that constant, so ATR(14) must read + // exactly 4.0 for every output element -- the seed is the mean of fourteen 4s, and each + // later bar is (4 * 13 + 4) / 14 = 4. + const int StartIdx = 0; + const int EndIdx = 199; + const double ExpectedAtr = 4.0; + double[] high = [.. Enumerable.Repeat(104.0, 200)]; + double[] low = [.. Enumerable.Repeat(100.0, 200)]; + double[] close = [.. Enumerable.Repeat(102.0, 200)]; + + // Act + AtrResult actualResult = TAMath.Atr(StartIdx, EndIdx, high, low, close, 14); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + actualResult.NBElement.ShouldBeGreaterThan(100); + + for (int k = 0; k < actualResult.NBElement; k++) + { + actualResult.Real[k].ShouldBe(ExpectedAtr, 1e-9, $"ATR drifted at output element {k}."); + } + } + + [Fact] + public void AtrDoesNotDivergeOverALongSeries() + { + // Arrange + // Regression guard for the accumulator that was never normalised: it grew by a factor of + // (period - 1) every bar, so ATR reached +Infinity a few hundred bars in. A sane ATR here + // is on the order of the bar range, never astronomically above it. + const int StartIdx = 0; + const int EndIdx = 1499; + double[] high = new double[1500]; + double[] low = new double[1500]; + double[] close = new double[1500]; + + for (int i = 0; i < 1500; i++) + { + double mid = 100.0 + (Math.Sin(i / 10.0) * 5.0); + high[i] = mid + 1.0; + low[i] = mid - 1.0; + close[i] = mid; + } + + // Act + AtrResult actualResult = TAMath.Atr(StartIdx, EndIdx, high, low, close, 14); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + + for (int k = 0; k < actualResult.NBElement; k++) + { + double atr = actualResult.Real[k]; + double.IsFinite(atr).ShouldBeTrue($"ATR was {atr} at output element {k}."); + atr.ShouldBeInRange(0.0, 20.0); + } + } } diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/Func/EmaTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/Func/EmaTests.cs index 9aa300f5..e910a8c3 100644 --- a/tests/TechnicalAnalysis.Functions.UnitTests/Func/EmaTests.cs +++ b/tests/TechnicalAnalysis.Functions.UnitTests/Func/EmaTests.cs @@ -47,4 +47,50 @@ public void EmaFloat() actualResult.ShouldNotBeNull(); actualResult.RetCode.ShouldBe(RetCode.Success); } + + [Fact] + public void EmaSeedIsTheSimpleAverageOfTheFirstPeriodValues() + { + // Arrange + // The EMA seed is the SMA of the first `timePeriod` closes. Here that is + // (0 + 0 + 0 + 0 + 100) / 5 = 20, and with exactly five bars the seed IS the only output, + // so nothing downstream can mask a wrong seed. Dropping the last term from the sum while + // still dividing by 5 seeds 0 and then smooths once to 100 / 3 = 33.33, which this pins. + const int StartIdx = 0; + const int EndIdx = 4; + double[] real = [0.0, 0.0, 0.0, 0.0, 100.0]; + + // Act + EmaResult actualResult = TAMath.Ema(StartIdx, EndIdx, real, 5); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + actualResult.NBElement.ShouldBe(1); + actualResult.Real[0].ShouldBe(20.0, 1e-9); + } + + [Fact] + public void EmaOnConstantSeriesReturnsThatConstant() + { + // Arrange + // Smoothing a constant can only ever return that constant: the seed is the mean of twenty + // 100s, and every later bar is prev + k * (100 - prev) with prev already 100. A seed that + // averages nineteen values over a divisor of twenty starts at 95 and smooths to 95.476190, + // which is what this asserts against. + const int StartIdx = 0; + const int EndIdx = 99; + double[] real = [.. Enumerable.Repeat(100.0, 100)]; + + // Act + EmaResult actualResult = TAMath.Ema(StartIdx, EndIdx, real, 20); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + actualResult.NBElement.ShouldBe(81); + + for (int k = 0; k < actualResult.NBElement; k++) + { + actualResult.Real[k].ShouldBe(100.0, 1e-9, $"EMA drifted at output element {k}."); + } + } } diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/Func/MacdTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/Func/MacdTests.cs index 08f54ed2..d0ac73a2 100644 --- a/tests/TechnicalAnalysis.Functions.UnitTests/Func/MacdTests.cs +++ b/tests/TechnicalAnalysis.Functions.UnitTests/Func/MacdTests.cs @@ -47,4 +47,32 @@ public void MacdFloat() actualResult.ShouldNotBeNull(); actualResult.RetCode.ShouldBe(RetCode.Success); } + + [Fact] + public void MacdOnConstantSeriesIsZero() + { + // Arrange + // MACD is the difference of two EMAs of the same series, so on a constant series both legs + // are that constant and all three outputs must be exactly zero. This is the propagation + // guard for the shared EMA seed: when the seed was scaled by (period - 1) / period the two + // legs were mis-seeded by *different* amounts (12 versus 26), and the difference did not + // cancel -- MACD read 0.53 on a series that never moved. + const int StartIdx = 0; + const int EndIdx = 199; + double[] real = [.. Enumerable.Repeat(100.0, 200)]; + + // Act + MacdResult actualResult = TAMath.Macd(StartIdx, EndIdx, real, 12, 26, 9); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + actualResult.NBElement.ShouldBeGreaterThan(0); + + for (int k = 0; k < actualResult.NBElement; k++) + { + actualResult.MacdValue[k].ShouldBe(0.0, 1e-9, $"MACD line was non-zero at element {k}."); + actualResult.MacdSignal[k].ShouldBe(0.0, 1e-9, $"MACD signal was non-zero at element {k}."); + actualResult.MacdHist[k].ShouldBe(0.0, 1e-9, $"MACD histogram was non-zero at element {k}."); + } + } } diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/Func/RsiTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/Func/RsiTests.cs index e2bbe091..20af28e4 100644 --- a/tests/TechnicalAnalysis.Functions.UnitTests/Func/RsiTests.cs +++ b/tests/TechnicalAnalysis.Functions.UnitTests/Func/RsiTests.cs @@ -47,4 +47,59 @@ public void RsiFloat() actualResult.ShouldNotBeNull(); actualResult.RetCode.ShouldBe(RetCode.Success); } + + [Fact] + public void RsiOnFlatSeriesReturnsZeroRatherThanNaN() + { + // Arrange + // A halted instrument produces a window with no price movement, so the average gain and the + // average loss are both exactly zero and the ratio is 0 / 0. Unguarded that is NaN returned + // next to RetCode.Success, which is worse than an error because it reads as a value. + const int StartIdx = 0; + const int EndIdx = 99; + double[] real = [.. Enumerable.Repeat(42.0, 100)]; + + // Act + RsiResult actualResult = TAMath.Rsi(StartIdx, EndIdx, real, 14); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + actualResult.NBElement.ShouldBeGreaterThan(0); + + for (int k = 0; k < actualResult.NBElement; k++) + { + double rsi = actualResult.Real[k]; + double.IsNaN(rsi).ShouldBeFalse($"RSI was NaN at output element {k}."); + rsi.ShouldBe(0.0, 1e-9); + } + } + + [Fact] + public void RsiOnMonotonicallyRisingSeriesReturnsOneHundred() + { + // Arrange + // Every bar gains and none loses, so the average loss stays zero and RSI is + // 100 * gain / (gain + 0) = 100 exactly. This is the opposite boundary to the flat series + // and confirms the zero guard did not swallow the legitimate zero-loss case. + const int StartIdx = 0; + const int EndIdx = 99; + double[] real = new double[100]; + + for (int i = 0; i < 100; i++) + { + real[i] = i + 1; + } + + // Act + RsiResult actualResult = TAMath.Rsi(StartIdx, EndIdx, real, 14); + + // Assert + actualResult.RetCode.ShouldBe(RetCode.Success); + actualResult.NBElement.ShouldBeGreaterThan(0); + + for (int k = 0; k < actualResult.NBElement; k++) + { + actualResult.Real[k].ShouldBe(100.0, 1e-9, $"RSI was not 100 at output element {k}."); + } + } } diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/AlignmentRegressionTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/AlignmentRegressionTests.cs new file mode 100644 index 00000000..bcb7718f --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/AlignmentRegressionTests.cs @@ -0,0 +1,191 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// This file is the regression suite for the historical alignment bug and is meant to be readable on +/// its own as proof that the bug cannot recur. +/// +/// THE BUG. TA-Lib fills its output array from index 0, while IndicatorResult.BegIdx is an index +/// into the INPUT series. Output element k therefore describes bar BegIdx + k: the last +/// valid ARRAY index is NBElement - 1 and the last valid BAR index is BegIdx + NBElement - 1. +/// The abandoned high-level API read Values[BegIdx + NBElement - 1] for every "current value" +/// property, i.e. it subscripted the array with a bar index. For SMA(30) over closes 1..100 that is +/// Values[29 + 71 - 1] == Values[99], an untouched zero slot, so it silently returned 0.0 and every +/// derived predicate was computed from that zero. Its 32 tests all passed because none of them asserted +/// a computed value. +/// +public class AlignmentRegressionTests +{ + // The exact fixture the historical bug was measured on: closes 1.0 .. 100.0. + // TAMath.Sma(0, 99, closes, 30) reports BegIdx = 29, NBElement = 71. + private static double[] ClosesOneToOneHundred() + { + double[] closes = new double[100]; + for (int i = 0; i < 100; i++) + { + closes[i] = i + 1; + } + + return closes; + } + + // The abandoned branch's expression, written out verbatim so the test names the failure mode. + // Values is sliced to Count, so this now indexes past the end instead of reading padding. + private static double ReadTheAbandonedBranchWay(IndicatorSeries series) + { + return series.WarmValues[series.FirstBar!.Value + series.WarmCount - 1]; + } + + [Fact] + public void Sma30OverClosesOneToOneHundredHasLatest85Point5AndNotZero() + { + // Arrange + // SMA(30) at the last bar is the mean of the final thirty closes, 71..100. + // Sum = (71 + 100) * 30 / 2 = 2565; 2565 / 30 = 85.5. + // The abandoned API returned 0.0 here, which is why the "not zero" assertion is separate. + PriceSeries prices = PriceSeries.FromClose(ClosesOneToOneHundred()); + + // Act + IndicatorSeries sma = prices.Sma(30); + + // Assert + sma.Latest.ShouldBe(85.5); + sma.Latest.ShouldNotBe(0.0); + sma.FirstBar.ShouldBe(29); + sma.LastBar.ShouldBe(99); + sma.WarmCount.ShouldBe(71); + sma.BarCount.ShouldBe(100); + sma.RetCode.ShouldBe(RetCode.Success); + } + + [Fact] + public void Sma30ValuesLandOnTheCorrectBarIndices() + { + // Arrange + // bar 29 -> mean of closes 1..30 = (1 + 30) / 2 = 15.5 (the first warm bar) + // bar 50 -> mean of closes 22..51 = (22 + 51) / 2 = 36.5 + // bar 99 -> mean of closes 71..100 = (71 + 100) / 2 = 85.5 + // Note bar 50 sits at ARRAY index 50 - 29 = 21; a test that passed while the two index + // spaces were confused would have to read 36.5 out of array slot 50, which holds 65.5. + PriceSeries prices = PriceSeries.FromClose(ClosesOneToOneHundred()); + IndicatorSeries sma = prices.Sma(30); + + // Act + double? atFirstWarmBar = sma[29]; + double? atMiddleBar = sma[50]; + double? atLastBar = sma[99]; + + // Assert + atFirstWarmBar.HasValue.ShouldBeTrue(); + atMiddleBar.HasValue.ShouldBeTrue(); + atLastBar.HasValue.ShouldBeTrue(); + atFirstWarmBar.ShouldBe(15.5); + atMiddleBar.ShouldBe(36.5); + atLastBar.ShouldBe(85.5); + + // The array-index and bar-index spaces are not interchangeable: array slot 50 is bar 79, + // whose value is the mean of closes 51..80 = 65.5, not 36.5. + sma.WarmValues[50].ShouldBe(65.5); + sma[79].ShouldBe(65.5); + } + + [Fact] + public void BarsBeforeTheWarmUpAreNullAndNotWarm() + { + // Arrange + // SMA(30) needs thirty closes, so bars 0..28 have no value at all. Absence is null: + // never 0.0, never NaN, and never an exception for an in-range bar. + PriceSeries prices = PriceSeries.FromClose(ClosesOneToOneHundred()); + IndicatorSeries sma = prices.Sma(30); + + // Act + double? atBarZero = sma[0]; + double? atLastColdBar = sma[28]; + + // Assert + atBarZero.ShouldBeNull(); + atLastColdBar.ShouldBeNull(); + sma.IsWarmAt(28).ShouldBeFalse(); + sma.IsWarmAt(29).ShouldBeTrue(); + } + + [Fact] + public void BarsOutsideTheSeriesThrowArgumentOutOfRange() + { + // Arrange + // "Bar 5 of a 30-period SMA" is a legitimate question answered null. + // "Bar 100 of a 100-bar series" is a caller bug and throws. + PriceSeries prices = PriceSeries.FromClose(ClosesOneToOneHundred()); + IndicatorSeries sma = prices.Sma(30); + + // Act + ArgumentOutOfRangeException belowRange = Should.Throw(() => + { + _ = sma[-1]; + }); + + ArgumentOutOfRangeException aboveRange = Should.Throw(() => + { + _ = sma[100]; + }); + + // Assert + belowRange.ParamName.ShouldBe("bar"); + aboveRange.ParamName.ShouldBe("bar"); + } + + [Fact] + public void TheAbandonedBranchExpressionNowThrowsInsteadOfReturningPadding() + { + // Arrange + // Values is sliced to exactly Count, so the padding is unreachable. The old expression + // Values[BegIdx + NBElement - 1] is Values[29 + 71 - 1] = Values[99] over a 71-element + // span, which raises IndexOutOfRangeException rather than quietly returning 0.0. + PriceSeries prices = PriceSeries.FromClose(ClosesOneToOneHundred()); + IndicatorSeries sma = prices.Sma(30); + + // Act + int valuesLength = sma.WarmValues.Length; + double firstValue = sma.WarmValues[0]; + double lastValue = sma.WarmValues[70]; + + // Assert + valuesLength.ShouldBe(71); + firstValue.ShouldBe(15.5); + lastValue.ShouldBe(85.5); + Should.Throw(() => + { + _ = ReadTheAbandonedBranchWay(sma); + }); + } + + [Fact] + public void RetCodeSuccessDoesNotMeanTheSeriesHasValues() + { + // Arrange + // TAMath.Sma(0, 4, fiveBars, 30) returns Success with BegIdx = 0 and NBElement = 0. + // BegIdx is a lie in that state, so IsEmpty (equivalently Count == 0) is the only warmth + // test. This is the assertion the abandoned branch's 32 tests were missing. + double[] fiveBars = [1.0, 2.0, 3.0, 4.0, 5.0]; + PriceSeries prices = PriceSeries.FromClose(fiveBars); + + // Act + IndicatorSeries sma = prices.Sma(30); + + // Assert + sma.RetCode.ShouldBe(RetCode.Success); + sma.HasValues.ShouldBeFalse(); + sma.WarmCount.ShouldBe(0); + sma.BarCount.ShouldBe(5); + sma.FirstBar.ShouldBeNull(); + sma.LastBar.ShouldBeNull(); + sma.Latest.ShouldBeNull(); + sma[4].ShouldBeNull(); + sma.WarmValues.Length.ShouldBe(0); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/DirectionalMovementTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/DirectionalMovementTests.cs new file mode 100644 index 00000000..9e3bd088 --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/DirectionalMovementTests.cs @@ -0,0 +1,264 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// +/// The regression suite for the directional-movement loop-translation defect. +/// +/// +/// THE DEFECT. The reference implementation seeds with while (i-- > 0) and +/// while (i-- != 0), each of which executes its body exactly i times. Adx, Dx, PlusDI +/// and MinusDI translated both as while (true) { i--; if (i <= 0) break; ... }, which +/// executes the body i - 1 times. Each function has two such loops, so today finished +/// two bars short of startIdx: the tail loop then emitted two extra values, every value was +/// computed from a window shifted two bars early, and outBegIdx + outNBElement came out two +/// past the end of the input. All four still reported , and the only +/// pre-existing tests asserted exactly that. +/// +/// +/// Over 100 bars with a period of 14 the defect produced Adx BegIdx = 27 NBElement = 75 (sum 102) +/// and Dx / PlusDI / MinusDI BegIdx = 14 NBElement = 88 (sum 102), against bar counts of 100. The +/// expected values below come from an independent transcription of the reference algorithm, and the +/// rising fixture's answers are additionally derivable on paper β€” see each test. +/// +/// +public class DirectionalMovementTests +{ + private const int Period = 14; + + private const int BarCount = 100; + + // A series that rises by exactly one point a bar, with a range of two points around the close. + // Every bar has diffP = +1 and diffM = -1, so +DM accumulates 1 a bar and -DM stays 0, and the + // true range is a constant 2. The smoothed +DM converges on 14 and the smoothed TR on 28, and + // the recursions keep TR exactly twice +DM in binary floating point, so: + // +DI = 100 * (+DM / TR) = 50 exactly -DI = 0 exactly + // DX = 100 * |0 - 50| / (0 + 50) = 100 exactly, and therefore ADX = 100 exactly. + private static (double[] High, double[] Low, double[] Close) Rising() + { + double[] high = new double[BarCount]; + double[] low = new double[BarCount]; + double[] close = new double[BarCount]; + + for (int bar = 0; bar < BarCount; bar++) + { + high[bar] = bar + 2.0; + low[bar] = bar; + close[bar] = bar + 1.0; + } + + return (high, low, close); + } + + // A triangular wave of period 20 oscillating between 50 and 60, with a range of four points + // around the close. Unlike the rising fixture, no two warm bars share an answer, so a one-bar + // shift changes every number rather than none of them. + private static (double[] High, double[] Low, double[] Close) ZigZag() + { + double[] high = new double[BarCount]; + double[] low = new double[BarCount]; + double[] close = new double[BarCount]; + + for (int bar = 0; bar < BarCount; bar++) + { + int phase = bar % 20; + double mid = 50.0 + (phase < 10 ? phase : 20 - phase); + close[bar] = mid; + high[bar] = mid + 2.0; + low[bar] = mid - 2.0; + } + + return (high, low, close); + } + + [Fact] + public void AdxMetadataFitsInsideThePriceSeriesInsteadOfOverrunningItByTwo() + { + // Arrange + // The lookback of ADX(14) with an unstable period of 0 is 2 * 14 + 0 - 1 = 27, so the first + // value describes bar 27 and there is one value for every remaining bar: + // 100 - 27 = 73. The defect reported 75 and 27 + 75 = 102 > 100. + (double[] high, double[] low, double[] close) = ZigZag(); + + // Act + AdxResult adx = TAMath.Adx(0, BarCount - 1, high, low, close, Period); + DxResult dx = TAMath.Dx(0, BarCount - 1, high, low, close, Period); + PlusDIResult plusDI = TAMath.PlusDI(0, BarCount - 1, high, low, close, Period); + MinusDIResult minusDI = TAMath.MinusDI(0, BarCount - 1, high, low, close, Period); + + // Assert + adx.RetCode.ShouldBe(RetCode.Success); + adx.BegIdx.ShouldBe(27); + adx.NBElement.ShouldBe(73); + (adx.BegIdx + adx.NBElement).ShouldBe(BarCount); + + // Dx, PlusDI and MinusDI all have a lookback of period + unstable = 14. + foreach (IndicatorResult result in new IndicatorResult[] { dx, plusDI, minusDI }) + { + result.RetCode.ShouldBe(RetCode.Success); + result.BegIdx.ShouldBe(14); + result.NBElement.ShouldBe(86); + (result.BegIdx + result.NBElement).ShouldBe(BarCount); + } + } + + [Fact] + public void OnAStrictlyRisingSeriesTheDirectionalIndicatorsTakeTheirClosedFormValues() + { + // Arrange + // Derived on paper in the Rising() comment: +DI = 50, -DI = 0, DX = 100, ADX = 100, all + // exactly, on every warm bar. Under the defect ADX read 92.85714285714286 at its first bar + // and only converged back towards 100, so the first-bar assertion alone catches it. + (double[] high, double[] low, double[] close) = Rising(); + PriceSeries prices = PriceSeries.FromHlc(high, low, close); + + // Act + IndicatorSeries adx = prices.Adx(Period); + PlusDIResult plusDI = TAMath.PlusDI(0, BarCount - 1, high, low, close, Period); + MinusDIResult minusDI = TAMath.MinusDI(0, BarCount - 1, high, low, close, Period); + DxResult dx = TAMath.Dx(0, BarCount - 1, high, low, close, Period); + + // Assert + adx.FirstBar.ShouldBe(27); + adx.LastBar.ShouldBe(99); + adx.WarmCount.ShouldBe(73); + adx[27].ShouldBe(100.0); + adx[99].ShouldBe(100.0); + + foreach ((int bar, double value) in adx) + { + value.ShouldBe(100.0, $"ADX was {value} at bar {bar}."); + } + + plusDI.Real[0].ShouldBe(50.0); + plusDI.Real[plusDI.NBElement - 1].ShouldBe(50.0); + minusDI.Real[0].ShouldBe(0.0); + minusDI.Real[minusDI.NBElement - 1].ShouldBe(0.0); + dx.Real[0].ShouldBe(100.0); + dx.Real[dx.NBElement - 1].ShouldBe(100.0); + } + + [Fact] + public void OnAZigZagSeriesEveryValueMatchesAnIndependentTranscriptionOfTheReference() + { + // Arrange + // These constants come from a separate transcription of the reference C, not from a + // previous run of this library. The defect's answers were 16.50835398166515 at ADX bar 27 + // and 66.66666666666669 at DX bar 14 -- 1.5 and 24.6 points out respectively -- so every + // one of these assertions discriminates. + (double[] high, double[] low, double[] close) = ZigZag(); + PriceSeries prices = PriceSeries.FromHlc(high, low, close); + const double Tolerance = 1e-12; + + // Act + IndicatorSeries adx = prices.Adx(Period); + DxResult dx = TAMath.Dx(0, BarCount - 1, high, low, close, Period); + PlusDIResult plusDI = TAMath.PlusDI(0, BarCount - 1, high, low, close, Period); + MinusDIResult minusDI = TAMath.MinusDI(0, BarCount - 1, high, low, close, Period); + + // Assert + adx[27].ShouldNotBeNull(); + adx[27].Value.ShouldBe(18.038560807106332, Tolerance); + adx[50].Value.ShouldBe(20.906071381560533, Tolerance); + adx[99].Value.ShouldBe(18.359235977556096, Tolerance); + + // Bar b sits at array index b - BegIdx; DX, +DI and -DI all begin at bar 14. + dx.Real[0].ShouldBe(42.07650273224045, Tolerance); + dx.Real[50 - 14].ShouldBe(38.300491054732625, Tolerance); + dx.Real[99 - 14].ShouldBe(30.413513993573694, Tolerance); + + plusDI.Real[0].ShouldBe(17.75956284153006, Tolerance); + plusDI.Real[50 - 14].ShouldBe(17.287561381841577, Tolerance); + plusDI.Real[99 - 14].ShouldBe(8.698310750803287, Tolerance); + + minusDI.Real[0].ShouldBe(7.240437158469945, Tolerance); + minusDI.Real[50 - 14].ShouldBe(7.712438618158421, Tolerance); + minusDI.Real[99 - 14].ShouldBe(16.30168924919671, Tolerance); + } + + [Fact] + public void TheFluentAdxDiscriminatesBarByBarSoAShiftCannotHide() + { + // Arrange + // A fixture whose every warm bar carries a different answer is the only kind that can catch + // an off-by-one alignment; a constant series masks a shift completely. + (double[] high, double[] low, double[] close) = ZigZag(); + PriceSeries prices = PriceSeries.FromHlc(high, low, close); + + // Act + IndicatorSeries adx = prices.Adx(Period); + HashSet distinct = []; + foreach ((int _, double value) in adx) + { + distinct.Add(value); + } + + // Assert + adx.WarmCount.ShouldBe(73); + distinct.Count.ShouldBeGreaterThan(60); + adx[26].ShouldBeNull(); + adx[27].ShouldNotBe(adx[28]); + adx[50].ShouldNotBe(adx[51]); + } + + [Fact] + public void AdxRefusesCloseOnlyDataAndValidatesItsPeriod() + { + // Arrange + // Directional movement is defined by how a bar's range extends past the previous bar's, so + // there is nothing to compute without a high and a low. + PriceSeries closeOnly = PriceSeries.FromClose(ZigZag().Close); + (double[] high, double[] low, double[] close) = ZigZag(); + PriceSeries hlc = PriceSeries.FromHlc(high, low, close); + + // Act + ArgumentOutOfRangeException badPeriod = Should.Throw(() => + { + _ = hlc.Adx(1); + }); + + // Assert + Should.Throw(() => + { + _ = closeOnly.Adx(Period); + }); + + badPeriod.ParamName.ShouldBe("timePeriod"); + } + + [Fact] + public void AdxOverTooLittleDataIsEmptyAndSucceeds() + { + // Arrange + // ADX(14) needs 28 bars before its first value; twenty is not enough, and that is not an + // error. An absent value must be null rather than a plausible-looking zero. + double[] high = new double[20]; + double[] low = new double[20]; + double[] close = new double[20]; + for (int bar = 0; bar < 20; bar++) + { + high[bar] = bar + 2.0; + low[bar] = bar; + close[bar] = bar + 1.0; + } + + PriceSeries prices = PriceSeries.FromHlc(high, low, close); + + // Act + IndicatorSeries adx = prices.Adx(Period); + + // Assert + adx.RetCode.ShouldBe(RetCode.Success); + adx.HasValues.ShouldBeFalse(); + adx.WarmCount.ShouldBe(0); + adx.BarCount.ShouldBe(20); + adx.FirstBar.ShouldBeNull(); + adx.Latest.ShouldBeNull(); + adx[19].ShouldBeNull(); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/HighLevelValidationTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/HighLevelValidationTests.cs new file mode 100644 index 00000000..c60db18a --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/HighLevelValidationTests.cs @@ -0,0 +1,396 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Collections.Concurrent; +using System.Reflection; + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// Exception behaviour across every indicator, plus the three machine-checkable forms of the design +/// constraint itself: no public member names a raw TA-Lib index, absence is a nullable type rather +/// than a sentinel, and the whole surface is a set of values with no shared mutable state. +/// +public class HighLevelValidationTests +{ + private static readonly Type[] HighLevelPublicSurface = + [ + typeof(PriceSeries), + typeof(IndicatorSeries), + typeof(OverlapStudyIndicators), + typeof(MomentumIndicators), + typeof(VolatilityIndicators), + typeof(VolumeIndicators), + ]; + + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + private static double[] Constant(int count, double value) + { + double[] values = new double[count]; + Array.Fill(values, value); + return values; + } + + private static PriceSeries CloseOnly() + { + return PriceSeries.FromClose(Ramp(100)); + } + + private static PriceSeries Hlc() + { + return PriceSeries.FromHlc(Constant(100, 102.0), Constant(100, 98.0), Constant(100, 100.0)); + } + + private static IEnumerable<(string Member, string Parameter)> PublicParameters(Type type) + { + const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly; + + foreach (MethodInfo method in type.GetMethods(Flags)) + { + foreach (ParameterInfo parameter in method.GetParameters()) + { + yield return (method.Name, parameter.Name ?? string.Empty); + } + } + + foreach (ConstructorInfo constructor in type.GetConstructors(Flags)) + { + foreach (ParameterInfo parameter in constructor.GetParameters()) + { + yield return (".ctor", parameter.Name ?? string.Empty); + } + } + + foreach (PropertyInfo property in type.GetProperties(Flags)) + { + foreach (ParameterInfo parameter in property.GetIndexParameters()) + { + yield return (property.Name, parameter.Name ?? string.Empty); + } + } + } + + [Fact] + public void EveryPeriodBelowTwoIsRejectedUniformlyAcrossEveryIndicator() + { + // Arrange + // One rule for every period parameter of every indicator: [2, 100000]. This is deliberately + // stricter than TAMath, where Atr and Stoch accept 1 and Sma, Ema, Rsi and BollingerBands + // do not -- a single rule cannot be got wrong by the author of the next indicator. + PriceSeries closes = CloseOnly(); + PriceSeries hlc = Hlc(); + + // Act + List thrown = + [ + Should.Throw(() => { _ = closes.Sma(1); }), + Should.Throw(() => { _ = closes.Sma(0); }), + Should.Throw(() => { _ = closes.Sma(-1); }), + Should.Throw(() => { _ = closes.Ema(1); }), + Should.Throw(() => { _ = closes.Rsi(1); }), + Should.Throw(() => { _ = hlc.Atr(1); }), + Should.Throw(() => { _ = hlc.Adx(1); }), + Should.Throw(() => { _ = closes.BollingerBands(1); }), + ]; + + // Assert + foreach (ArgumentOutOfRangeException exception in thrown) + { + exception.ParamName.ShouldBe("timePeriod"); + } + } + + [Fact] + public void APeriodAboveTheUpperBoundIsRejected() + { + // Arrange + // ValidationHelper.MaxPeriod is 100000, so 100001 is one past the boundary. + PriceSeries closes = CloseOnly(); + + // Act + ArgumentOutOfRangeException tooLarge = Should.Throw(() => + { + _ = closes.Sma(100001); + }); + + // Assert + tooLarge.ParamName.ShouldBe("timePeriod"); + } + + [Fact] + public void EveryMacdPeriodIsValidatedIndividually() + { + // Arrange + // The signalPeriod case is the important one: TAMath.Macd accepts a signal period of 1 and + // then throws ArgumentOutOfRangeException from Array.Copy inside TA_INT_MACD rather than + // returning a code. The facade must refuse before TAMath is reached. + PriceSeries closes = CloseOnly(); + + // Act + ArgumentOutOfRangeException fast = Should.Throw(() => + { + _ = closes.Macd(1, 26, 9); + }); + + ArgumentOutOfRangeException slow = Should.Throw(() => + { + _ = closes.Macd(12, 1, 9); + }); + + ArgumentOutOfRangeException signal = Should.Throw(() => + { + _ = closes.Macd(12, 26, 1); + }); + + // Assert + fast.ParamName.ShouldBe("fastPeriod"); + slow.ParamName.ShouldBe("slowPeriod"); + signal.ParamName.ShouldBe("signalPeriod"); + } + + [Fact] + public void EveryStochPeriodIsValidatedIndividually() + { + // Arrange + PriceSeries hlc = Hlc(); + + // Act + ArgumentOutOfRangeException fastK = Should.Throw(() => + { + _ = hlc.Stoch(1, 3, MAType.Sma, 3, MAType.Sma); + }); + + ArgumentOutOfRangeException slowK = Should.Throw(() => + { + _ = hlc.Stoch(5, 1, MAType.Sma, 3, MAType.Sma); + }); + + ArgumentOutOfRangeException slowD = Should.Throw(() => + { + _ = hlc.Stoch(5, 3, MAType.Sma, 1, MAType.Sma); + }); + + // Assert + fastK.ParamName.ShouldBe("fastKPeriod"); + slowK.ParamName.ShouldBe("slowKPeriod"); + slowD.ParamName.ShouldBe("slowDPeriod"); + } + + [Fact] + public void PeriodValidationHappensBeforeEmptinessAndBeforeTheOhlcRequirement() + { + // Arrange + // The order is part of the contract: period, then component availability, then the empty + // short-circuit. So a bad period always reports itself as a bad period, whatever else is + // also wrong with the call. + PriceSeries empty = PriceSeries.Empty; + PriceSeries closeOnly = CloseOnly(); + + // Act + ArgumentOutOfRangeException onEmpty = Should.Throw(() => + { + _ = empty.Sma(1); + }); + + ArgumentOutOfRangeException onCloseOnly = Should.Throw(() => + { + _ = closeOnly.Atr(1); + }); + + // Assert + onEmpty.ParamName.ShouldBe("timePeriod"); + onCloseOnly.ParamName.ShouldBe("timePeriod"); + } + + [Fact] + public void DeviationsAreNotValidatedAndZeroCollapsesTheBandsOntoTheMiddle() + { + // Arrange + // TA-Lib accepts any finite deviation, so the facade adds no rule of its own. With both + // deviations 0 the envelope has no width and all three bands read the 20-period mean of + // closes 81..100 = 90.5. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + BollingerBandsSeries bands = prices.BollingerBands(20, 0.0, 0.0); + + // Assert + bands.Upper.Latest.ShouldBe(90.5); + bands.Middle.Latest.ShouldBe(90.5); + bands.Lower.Latest.ShouldBe(90.5); + } + + [Fact] + public void NoPublicMemberNamesARawTaLibIndex() + { + // Arrange + // This is the machine-checkable form of the whole design constraint. startIdx and endIdx do + // not exist on this surface at all -- every indicator analyses the full range and windowing + // is expressed by AsOf. begIdx and nbElement appear in exactly one place, the single point + // at which raw TA-Lib metadata enters the type system. + List<(string Type, string Member, string Parameter)> parameters = []; + foreach (Type type in HighLevelPublicSurface) + { + foreach ((string member, string parameter) in PublicParameters(type)) + { + parameters.Add((type.Name, member, parameter)); + } + } + + // Act + List<(string Type, string Member, string Parameter)> rawIndexParameters = + [.. parameters.Where(p => p.Parameter is "startIdx" or "endIdx")]; + + List<(string Type, string Member, string Parameter)> rawMetadataParameters = + [.. parameters.Where(p => p.Parameter is "begIdx" or "nbElement")]; + + // Assert + parameters.ShouldNotBeEmpty(); + rawIndexParameters.ShouldBeEmpty(); + rawMetadataParameters.ShouldAllBe(p => p.Type == nameof(IndicatorSeries) && p.Member == "Create"); + rawMetadataParameters.Count.ShouldBe(2); + } + + [Fact] + public void NoArraySpaceMemberWearsABarSpaceName() + { + // Arrange + // IndicatorSeries has two index spaces and one indexer, so the names that read as bar space + // must not be occupied by array-space members. + // Count reads as "the number of valid indices" on any type with an indexer, and the + // indexer's domain here is [0, BarCount) -- so the array-space count is WarmCount + // and `for (i = 0; i < s.Count; i++) s[i]` cannot be written at all. + // Values is the first name a caller reaches for, and series.Values[bar] would compile + // and return a plausible number for the wrong bar -- so it is WarmValues. + // IsEmpty means "no bars" on PriceSeries; here it would mean "no values" on a type that + // also has a BarCount, so the warmth test is HasValues. + Type series = typeof(IndicatorSeries); + const BindingFlags Flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; + + // Act + string[] memberNames = [.. series.GetMembers(Flags).Select(m => m.Name)]; + + // Assert + memberNames.ShouldContain(nameof(IndicatorSeries.WarmCount)); + memberNames.ShouldContain(nameof(IndicatorSeries.WarmValues)); + memberNames.ShouldContain(nameof(IndicatorSeries.HasValues)); + memberNames.ShouldNotContain("Count"); + memberNames.ShouldNotContain("Values"); + memberNames.ShouldNotContain("IsEmpty"); + + // PriceSeries keeps IsEmpty, where it unambiguously means "no bars". + typeof(PriceSeries).GetProperty(nameof(PriceSeries.IsEmpty)).ShouldNotBeNull(); + } + + [Fact] + public void AbsenceIsANullableTypeRatherThanASentinelValue() + { + // Arrange + // A double? is discovered by the compiler; a NaN or a 0.0 is discovered in production. + // Making "no value" a distinct type is the strongest available fix for a bug whose whole + // shape was "a plausible-looking number where there was no value". + Type type = typeof(IndicatorSeries); + + // Act + PropertyInfo indexer = type + .GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly) + .Single(p => p.GetIndexParameters().Length == 1); + PropertyInfo? latest = type.GetProperty(nameof(IndicatorSeries.Latest)); + PropertyInfo? firstBar = type.GetProperty(nameof(IndicatorSeries.FirstBar)); + PropertyInfo? lastBar = type.GetProperty(nameof(IndicatorSeries.LastBar)); + + // Assert + indexer.PropertyType.ShouldBe(typeof(double?)); + indexer.GetIndexParameters()[0].ParameterType.ShouldBe(typeof(int)); + latest.ShouldNotBeNull(); + latest.PropertyType.ShouldBe(typeof(double?)); + firstBar.ShouldNotBeNull(); + firstBar.PropertyType.ShouldBe(typeof(int?)); + lastBar.ShouldNotBeNull(); + lastBar.PropertyType.ShouldBe(typeof(int?)); + } + + [Fact] + public void EqualityIsReferenceIdentityOverTheBackingArray() + { + // Arrange + // Two separate calls compute two separate output arrays, so the series are not equal even + // though every value agrees. AsOf allocates nothing and keeps the same array, so narrowing + // to the last bar is genuinely the same value. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + IndicatorSeries first = prices.Sma(30); + IndicatorSeries second = prices.Sma(30); + IndicatorSeries identity = first.AsOf(99); + + // Act + bool separateCallsEqual = first == second; +#pragma warning disable CS1718 // Comparison made to same variable -- reflexivity of operator== is exactly what is under test. + bool selfEqual = first == first; +#pragma warning restore CS1718 + bool narrowingIsIdentity = identity == first; + + // Assert + separateCallsEqual.ShouldBeFalse(); + first.Equals(second).ShouldBeFalse(); + first.Equals((object)second).ShouldBeFalse(); + (first != second).ShouldBeTrue(); + selfEqual.ShouldBeTrue(); + narrowingIsIdentity.ShouldBeTrue(); + + // Values agree even though the series do not, which is exactly what "not a value comparison" + // means. If this ever fails, the two series are not merely unequal -- they disagree. + first.Latest.ShouldBe(second.Latest); + + identity.GetHashCode().ShouldBe(first.GetHashCode()); + first.GetHashCode().ShouldBe(first.GetHashCode()); + first.GetHashCode().ShouldNotBe( + second.GetHashCode(), + "Two distinct backing arrays should hash differently; a genuine collision here is astronomically unlikely."); + } + + [Fact] + public void ConcurrentUseOfOneSharedPriceSeriesGivesTheSequentialAnswers() + { + // Arrange + // There is no cache, no lazy field and no lock anywhere on this surface: a PriceSeries is an + // immutable copy and every IndicatorSeries wraps a freshly allocated array nothing else + // holds. So this must pass unconditionally, not merely usually. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + double? expectedSma = prices.Sma(30).Latest; + double? expectedRsi = prices.Rsi(14).Latest; + double? expectedMacd = prices.Macd().Line.Latest; + ConcurrentBag<(double? Sma, double? Rsi, double? Macd)> observed = []; + + // Act + Parallel.For(0, 256, _ => + { + observed.Add((prices.Sma(30).Latest, prices.Rsi(14).Latest, prices.Macd().Line.Latest)); + }); + + // Assert + expectedSma.ShouldBe(85.5); + expectedRsi.ShouldBe(100.0); + expectedMacd.ShouldBe(7.0); + observed.Count.ShouldBe(256); + + foreach ((double? sma, double? rsi, double? macd) in observed) + { + sma.ShouldBe(expectedSma); + rsi.ShouldBe(expectedRsi); + macd.ShouldBe(expectedMacd); + } + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesAsOfTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesAsOfTests.cs new file mode 100644 index 00000000..5c867538 --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesAsOfTests.cs @@ -0,0 +1,226 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// Causal narrowing. AsOf(bar) returns a series whose BarCount is bar + 1, so the +/// future is not part of the value that was handed over: look-ahead is unrepresentable rather than +/// merely detected. These tests also pin the equivalence that makes it sound -- narrowing an +/// indicator computed over the whole series gives bit-identical values to recomputing the indicator +/// over the truncated price series. +/// +public class IndicatorSeriesAsOfTests +{ + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + private static PriceSeries ConstantRangeHlc(int barCount) + { + // high 102, low 98, close 100 on every bar: the true range is exactly 4.0 everywhere and the + // close sits exactly midway in the range, so ATR and Stoch have closed-form answers. + double[] high = new double[barCount]; + double[] low = new double[barCount]; + double[] close = new double[barCount]; + Array.Fill(high, 102.0); + Array.Fill(low, 98.0); + Array.Fill(close, 100.0); + + return PriceSeries.FromHlc(high, low, close); + } + + [Fact] + public void AsOfNarrowsTheAddressableRangeWithoutMovingAnyValue() + { + // Arrange + // SMA(30) over closes 1..100 is warm on bars 29..99. Narrowing to bar 50 keeps bars 29..50, + // i.e. 50 - 29 + 1 = 22 values, and the newest of those is the mean of closes 22..51 = 36.5. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + IndicatorSeries sma = prices.Sma(30); + + // Act + IndicatorSeries narrowed = sma.AsOf(50); + + // Assert + narrowed.BarCount.ShouldBe(51); + narrowed.LastBar.ShouldBe(50); + narrowed.Latest.ShouldBe(36.5); + narrowed.WarmCount.ShouldBe(22); + narrowed.FirstBar.ShouldBe(29); + narrowed[29].ShouldBe(15.5); + } + + [Fact] + public void AsOfMakesLookAheadUnrepresentable() + { + // Arrange + // After AsOf(50) the series has BarCount 51, so bar 51 is outside [0, BarCount) and asking + // for it is a caller bug -- not a null, and certainly not tomorrow's value. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + IndicatorSeries narrowed = prices.Sma(30).AsOf(50); + + // Act + ArgumentOutOfRangeException exception = Should.Throw(() => + { + _ = narrowed[51]; + }); + + // Assert + exception.ParamName.ShouldBe("bar"); + } + + [Fact] + public void AsOfAtTheLastBarIsTheIdentityAndOutOfRangeBarsThrow() + { + // Arrange + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + IndicatorSeries sma = prices.Sma(30); + + // Act + IndicatorSeries identity = sma.AsOf(99); + ArgumentOutOfRangeException past = Should.Throw(() => + { + _ = sma.AsOf(100); + }); + + ArgumentOutOfRangeException negative = Should.Throw(() => + { + _ = sma.AsOf(-1); + }); + + // Assert + (identity == sma).ShouldBeTrue(); + past.ParamName.ShouldBe("bar"); + negative.ParamName.ShouldBe("bar"); + } + + [Fact] + public void AsOfAtAndBeforeTheWarmUpEdge() + { + // Arrange + // Bar 29 is the first warm bar of SMA(30) over closes 1..100, value 15.5. + // Bar 28 and anything earlier leaves no value at all, which is an empty series -- not a + // series with a zero in it. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + IndicatorSeries sma = prices.Sma(30); + + // Act + IndicatorSeries atEdge = sma.AsOf(29); + IndicatorSeries justBeforeEdge = sma.AsOf(28); + IndicatorSeries wellBeforeEdge = sma.AsOf(10); + + // Assert + atEdge.WarmCount.ShouldBe(1); + atEdge.Latest.ShouldBe(15.5); + atEdge.LastBar.ShouldBe(29); + + justBeforeEdge.HasValues.ShouldBeFalse(); + justBeforeEdge.BarCount.ShouldBe(29); + justBeforeEdge.FirstBar.ShouldBeNull(); + justBeforeEdge.Latest.ShouldBeNull(); + + wellBeforeEdge.HasValues.ShouldBeFalse(); + wellBeforeEdge.BarCount.ShouldBe(11); + } + + [Fact] + public void SmaNarrowedEqualsSmaRecomputedOverTheNarrowedPrices() + { + // Arrange + // SMA is causal, so truncating the input cannot change an earlier output. Both routes must + // give the mean of closes 22..51 = 36.5, and they must agree exactly, not approximately. + double[] closes = Ramp(100); + + // Act + double? recomputed = PriceSeries.FromClose(closes).AsOf(50).Sma(30).Latest; + double? narrowed = PriceSeries.FromClose(closes).Sma(30).AsOf(50).Latest; + + // Assert + recomputed.ShouldBe(36.5); + narrowed.ShouldBe(36.5); + recomputed.ShouldBe(narrowed); + } + + [Fact] + public void RsiNarrowedEqualsRsiRecomputedOverTheNarrowedPrices() + { + // Arrange + // Closes 1..100 rise by exactly 1 every bar, so there is never a down-move: Wilder's + // average loss stays 0 and RSI = 100 * gain / (gain + 0) = 100.0 exactly on every warm bar. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + double? recomputed = prices.AsOf(60).Rsi(14).Latest; + double? fromFullSeries = prices.Rsi(14)[60]; + + // Assert + recomputed.ShouldBe(100.0); + fromFullSeries.ShouldBe(100.0); + recomputed.ShouldBe(fromFullSeries); + } + + [Fact] + public void MacdNarrowedEqualsMacdRecomputedOverTheNarrowedPrices() + { + // Arrange + // On a unit-slope ramp an N-period EMA seeded with the SMA of the first N closes sits + // exactly (N - 1) / 2 below the price. MACD = EMA(12) - EMA(26) = 12.5 - 5.5 = 7.0 on + // every warm bar, and the arithmetic is exact in binary floating point. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + double? recomputed = prices.AsOf(60).Macd().Line.Latest; + double? fromFullSeries = prices.Macd().Line[60]; + + // Assert + recomputed.ShouldBe(7.0); + fromFullSeries.ShouldBe(7.0); + recomputed.ShouldBe(fromFullSeries); + } + + [Fact] + public void AtrNarrowedEqualsAtrRecomputedAndMultiOutputAsOfNarrowsEveryComponent() + { + // Arrange + // Constant true range of 4.0 on every bar, so Wilder's average of it is exactly 4.0. + PriceSeries hlc = ConstantRangeHlc(60); + + // Act + double? recomputed = hlc.AsOf(40).Atr(14).Latest; + double? fromFullSeries = hlc.Atr(14)[40]; + + MacdSeries macd = PriceSeries.FromClose(Ramp(100)).Macd().AsOf(60); + BollingerBandsSeries bands = PriceSeries.FromClose(Ramp(100)).BollingerBands(20, 2.0, 2.0).AsOf(60); + StochSeries stoch = hlc.Stoch().AsOf(40); + + // Assert + recomputed.ShouldBe(4.0); + fromFullSeries.ShouldBe(4.0); + recomputed.ShouldBe(fromFullSeries); + + macd.Line.BarCount.ShouldBe(61); + macd.Signal.BarCount.ShouldBe(61); + macd.Histogram.BarCount.ShouldBe(61); + macd.Line.LastBar.ShouldBe(60); + + bands.Upper.BarCount.ShouldBe(61); + bands.Middle.BarCount.ShouldBe(61); + bands.Lower.BarCount.ShouldBe(61); + bands.Middle.LastBar.ShouldBe(60); + + stoch.SlowK.BarCount.ShouldBe(41); + stoch.SlowD.BarCount.ShouldBe(41); + stoch.SlowK.LastBar.ShouldBe(40); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesCrossingTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesCrossingTests.cs new file mode 100644 index 00000000..ab30dd8b --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesCrossingTests.cs @@ -0,0 +1,233 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// Crossings are mechanical, not opinionated: a crossing is a transition between two adjacent bars, +/// so it is false where the state was already reached and false wherever either of the two bars has +/// no value. Every fixture below pins its indicator values first, so a crossing assertion can never +/// be true for the wrong reason. +/// +public class IndicatorSeriesCrossingTests +{ + // SMA(2) over [10, 10, 10, 10, 20, 20]: warm from bar 1, values 10, 10, 10, 15, 20 on bars 1..5. + // The rise across the 12.0 level happens exactly once, between bar 3 (10) and bar 4 (15). + private static IndicatorSeries RisingFixture() + { + return PriceSeries.FromClose([10.0, 10.0, 10.0, 10.0, 20.0, 20.0]).Sma(2); + } + + // SMA(2) over [20, 20, 20, 20, 10, 10]: warm from bar 1, values 20, 20, 20, 15, 10 on bars 1..5. + private static IndicatorSeries FallingFixture() + { + return PriceSeries.FromClose([20.0, 20.0, 20.0, 20.0, 10.0, 10.0]).Sma(2); + } + + private static double[] EightBarCloses() + { + return [10.0, 10.0, 10.0, 10.0, 10.0, 30.0, 30.0, 30.0]; + } + + [Fact] + public void RisingFixtureHasTheValuesTheCrossingTestsRelyOn() + { + // Arrange + // bar 1 = (10 + 10) / 2 = 10, bar 2 = 10, bar 3 = 10, + // bar 4 = (10 + 20) / 2 = 15, bar 5 = (20 + 20) / 2 = 20. + IndicatorSeries x = RisingFixture(); + + // Act + // (the fixture itself is the subject) + + // Assert + x.FirstBar.ShouldBe(1); + x.BarCount.ShouldBe(6); + x[0].ShouldBeNull(); + x[1].ShouldBe(10.0); + x[2].ShouldBe(10.0); + x[3].ShouldBe(10.0); + x[4].ShouldBe(15.0); + x[5].ShouldBe(20.0); + } + + [Fact] + public void CrossedAboveIsTrueOnlyAtTheBarWhereTheLevelIsPassed() + { + // Arrange + // At bar 4 the value is 15 (> 12) while at bar 3 it was 10 (<= 12): that is the transition. + IndicatorSeries x = RisingFixture(); + + // Act + bool atCrossing = x.CrossedAbove(12.0, 4); + + // Assert + atCrossing.ShouldBeTrue(); + } + + [Fact] + public void CrossedAboveIsFalseWhileAlreadyAboveTheLevel() + { + // Arrange + // Bar 5 is 20 and bar 4 was already 15: being above is a state, crossing is a transition. + IndicatorSeries x = RisingFixture(); + + // Act + bool afterCrossing = x.CrossedAbove(12.0, 5); + + // Assert + afterCrossing.ShouldBeFalse(); + } + + [Fact] + public void CrossedAboveIsFalseBeforeTheCrossingAndCrossedBelowIsFalseOnARise() + { + // Arrange + // Bar 3 is 10 and bar 2 was 10: nothing happened. And a rise is never a fall. + IndicatorSeries x = RisingFixture(); + + // Act + bool beforeCrossing = x.CrossedAbove(12.0, 3); + bool wrongDirection = x.CrossedBelow(12.0, 4); + + // Assert + beforeCrossing.ShouldBeFalse(); + wrongDirection.ShouldBeFalse(); + } + + [Fact] + public void CrossingAtTheWarmUpEdgeIsFalseButAnOutOfRangeBarStillThrows() + { + // Arrange + // Bar 1 is warm but bar 0 is not, so there is no prior value and therefore no transition. + // Bar 0 has no predecessor at all. Neither is an error: they are statements about the data + // that is present. Bar 6 is outside [0, 6) and is a caller bug. + IndicatorSeries x = RisingFixture(); + + // Act + bool priorBarNotWarm = x.CrossedAbove(12.0, 1); + bool noPriorBarAtAll = x.CrossedAbove(12.0, 0); + ArgumentOutOfRangeException outOfRange = Should.Throw(() => + { + _ = x.CrossedAbove(12.0, 6); + }); + + // Assert + priorBarNotWarm.ShouldBeFalse(); + noPriorBarAtAll.ShouldBeFalse(); + outOfRange.ParamName.ShouldBe("bar"); + } + + [Fact] + public void CrossedBelowIsTrueOnlyAtTheBarWhereTheLevelIsPassedDownward() + { + // Arrange + // bar 4 = (20 + 10) / 2 = 15, bar 5 = (10 + 10) / 2 = 10. The fall through 12.0 is at bar 5. + IndicatorSeries x = FallingFixture(); + + // Act + bool beforeCrossing = x.CrossedBelow(12.0, 4); + bool atCrossing = x.CrossedBelow(12.0, 5); + bool wrongDirection = x.CrossedAbove(12.0, 5); + + // Assert + x[4].ShouldBe(15.0); + x[5].ShouldBe(10.0); + beforeCrossing.ShouldBeFalse(); + atCrossing.ShouldBeTrue(); + wrongDirection.ShouldBeFalse(); + } + + [Fact] + public void TwoSeriesFixtureHasTheValuesTheCrossingTestsRelyOn() + { + // Arrange + // Closes [10, 10, 10, 10, 10, 30, 30, 30]. + // SMA(2) warm from bar 1: 10, 10, 10, 10, 20, 30, 30 on bars 1..7. + // SMA(4) warm from bar 3: 10, 10, 15, 20, 25 on bars 3..7. + double[] closes = EightBarCloses(); + + // Act + IndicatorSeries fast = PriceSeries.FromClose(closes).Sma(2); + IndicatorSeries slow = PriceSeries.FromClose(closes).Sma(4); + + // Assert + fast.FirstBar.ShouldBe(1); + fast[1].ShouldBe(10.0); + fast[2].ShouldBe(10.0); + fast[3].ShouldBe(10.0); + fast[4].ShouldBe(10.0); + fast[5].ShouldBe(20.0); + fast[6].ShouldBe(30.0); + fast[7].ShouldBe(30.0); + + slow.FirstBar.ShouldBe(3); + slow[2].ShouldBeNull(); + slow[3].ShouldBe(10.0); + slow[4].ShouldBe(10.0); + slow[5].ShouldBe(15.0); + slow[6].ShouldBe(20.0); + slow[7].ShouldBe(25.0); + } + + [Fact] + public void FastCrossesSlowExactlyOnceAndTheReciprocalCrossingAgrees() + { + // Arrange + // At bar 5 fast is 20 and slow is 15 (fast above); at bar 4 both were 10 (fast <= slow). + // At bar 6 fast is 30 and slow is 20, but fast was already above at bar 5. + double[] closes = EightBarCloses(); + IndicatorSeries fast = PriceSeries.FromClose(closes).Sma(2); + IndicatorSeries slow = PriceSeries.FromClose(closes).Sma(4); + + // Act + bool atCrossing = fast.CrossedAbove(slow, 5); + bool afterCrossing = fast.CrossedAbove(slow, 6); + bool reciprocal = slow.CrossedBelow(fast, 5); + + // Assert + atCrossing.ShouldBeTrue(); + afterCrossing.ShouldBeFalse(); + reciprocal.ShouldBeTrue(); + } + + [Fact] + public void DifferingWarmUpsAreHandledAndMismatchedBarCountsAreRejected() + { + // Arrange + // At bar 3 the slow SMA is warm but at bar 2 it is not, so there is no transition to + // report -- the caller never has to reason about the two different warm-up lengths. + double[] closes = EightBarCloses(); + IndicatorSeries fast = PriceSeries.FromClose(closes).Sma(2); + IndicatorSeries slow = PriceSeries.FromClose(closes).Sma(4); + IndicatorSeries unrelated = PriceSeries.FromClose(RampOfOneHundred()).Sma(30); + + // Act + bool priorBarNotWarmInOther = fast.CrossedAbove(slow, 3); + bool survivesNarrowing = fast.AsOf(6).CrossedAbove(slow.AsOf(6), 5); + ArgumentException mismatched = Should.Throw(() => + { + _ = fast.CrossedAbove(unrelated, 5); + }); + + // Assert + priorBarNotWarmInOther.ShouldBeFalse(); + survivesNarrowing.ShouldBeTrue(); + mismatched.ShouldBeOfType(); + mismatched.ParamName.ShouldBe("other"); + } + + private static double[] RampOfOneHundred() + { + double[] values = new double[100]; + for (int i = 0; i < 100; i++) + { + values[i] = i + 1; + } + + return values; + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesTests.cs new file mode 100644 index 00000000..09c09bde --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/IndicatorSeriesTests.cs @@ -0,0 +1,420 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// The alignment primitive in isolation: every series here is built by hand through +/// with metadata written out explicitly, so the assertions +/// depend on nothing but the primitive itself. +/// +public class IndicatorSeriesTests +{ + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + [Fact] + public void CreateMapsArrayElementKToBarFirstBarPlusK() + { + // Arrange + // Three values that describe bars 7, 8 and 9 of a ten-bar series. The array is ten long + // because TAMath allocates endIdx - startIdx + 1 slots; everything from index 3 on is + // meaningless padding and must be unreachable. + double[] values = [1.0, 2.0, 3.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; + + // Act + IndicatorSeries series = IndicatorSeries.Create(RetCode.Success, begIdx: 7, nbElement: 3, values, barCount: 10); + + // Assert + series.FirstBar.ShouldBe(7); + series.LastBar.ShouldBe(9); + series.WarmCount.ShouldBe(3); + series.BarCount.ShouldBe(10); + series[7].ShouldBe(1.0); + series[8].ShouldBe(2.0); + series[9].ShouldBe(3.0); + series.Latest.ShouldBe(3.0); + series[6].ShouldBeNull(); + series[0].ShouldBeNull(); + series.WarmValues.Length.ShouldBe(3); + } + + [Fact] + public void CreateRejectsMetadataThatOverrunsTheBarCount() + { + // Arrange + // 7 + 4 = 11 > 10: the series claims a value for a bar the price series does not have. + // This is the check that caught the Adx / Dx / PlusDI / MinusDI loop-translation defect, + // which reported BegIdx + NBElement exactly two past the end (that defect is now fixed at + // its root; see DirectionalMovementTests). Clamping would hand back a silently + // two-bar-misaligned series, which is the failure this design exists to prevent. + double[] values = new double[10]; + + // Act + ArgumentException exception = Should.Throw(() => + { + _ = IndicatorSeries.Create(RetCode.Success, begIdx: 7, nbElement: 4, values, barCount: 10); + }); + + // Assert + // Exactly ArgumentException, not the ArgumentOutOfRangeException used for negative inputs: + // the arguments are individually in range and it is their combination that is impossible. + exception.ShouldBeOfType(); + exception.ParamName.ShouldBe("nbElement"); + } + + [Fact] + public void CreateValidatesItsArguments() + { + // Arrange + double[] values = new double[10]; + + // Act + ArgumentException tooManyElements = Should.Throw(() => + { + _ = IndicatorSeries.Create(RetCode.Success, begIdx: 0, nbElement: 11, values, barCount: 20); + }); + + ArgumentNullException nullValues = Should.Throw(() => + { + _ = IndicatorSeries.Create(RetCode.Success, begIdx: 0, nbElement: 1, values: null!, barCount: 10); + }); + + ArgumentOutOfRangeException negativeBegIdx = Should.Throw(() => + { + _ = IndicatorSeries.Create(RetCode.Success, begIdx: -1, nbElement: 1, values, barCount: 10); + }); + + ArgumentOutOfRangeException negativeNbElement = Should.Throw(() => + { + _ = IndicatorSeries.Create(RetCode.Success, begIdx: 0, nbElement: -1, values, barCount: 10); + }); + + ArgumentOutOfRangeException negativeBarCount = Should.Throw(() => + { + _ = IndicatorSeries.Create(RetCode.Success, begIdx: 0, nbElement: 1, values, barCount: -1); + }); + + // Assert + tooManyElements.ShouldBeOfType(); + tooManyElements.ParamName.ShouldBe("nbElement"); + nullValues.ParamName.ShouldBe("values"); + negativeBegIdx.ParamName.ShouldBe("begIdx"); + negativeNbElement.ParamName.ShouldBe("nbElement"); + negativeBarCount.ParamName.ShouldBe("barCount"); + } + + [Fact] + public void CreateWithZeroElementsPreservesTheRetCodeAndNeverFabricatesABar() + { + // Arrange + // A failure must not invent a first bar. BegIdx is not even examined when nbElement is 0, + // because TA-Lib routinely reports BegIdx = 0 alongside NBElement = 0. + double[] values = new double[10]; + + // Act + IndicatorSeries series = IndicatorSeries.Create(RetCode.BadParam, begIdx: 0, nbElement: 0, values, barCount: 10); + + // Assert + series.HasValues.ShouldBeFalse(); + series.WarmCount.ShouldBe(0); + series.BarCount.ShouldBe(10); + series.RetCode.ShouldBe(RetCode.BadParam); + series.FirstBar.ShouldBeNull(); + } + + [Fact] + public void EmptyCarriesABarCountButNoValues() + { + // Arrange + const int BarCount = 7; + + // Act + IndicatorSeries series = IndicatorSeries.Empty(BarCount); + ArgumentOutOfRangeException negative = Should.Throw(() => + { + _ = IndicatorSeries.Empty(-1); + }); + + // Assert + series.BarCount.ShouldBe(7); + series.WarmCount.ShouldBe(0); + series.RetCode.ShouldBe(RetCode.Success); + series.FirstBar.ShouldBeNull(); + negative.ParamName.ShouldBe("barCount"); + } + + [Fact] + public void DefaultInstancesAreValidEmptyValues() + { + // Arrange + // RetCode.Success is 0, so a default IndicatorSeries reports Success. That is exactly the + // trap TA-Lib itself sets, and it is why IsEmpty rather than RetCode is the warmth test. + IndicatorSeries series = default; + PriceSeries prices = default; + + // Act + // (nothing to do -- the values under test are the defaults themselves) + + // Assert + series.HasValues.ShouldBeFalse(); + series.BarCount.ShouldBe(0); + series.WarmCount.ShouldBe(0); + series.RetCode.ShouldBe(RetCode.Success); + series.FirstBar.ShouldBeNull(); + series.Latest.ShouldBeNull(); + series.WarmValues.Length.ShouldBe(0); + + prices.BarCount.ShouldBe(0); + prices.IsEmpty.ShouldBeTrue(); + prices.HasOpen.ShouldBeFalse(); + prices.HasHighLow.ShouldBeFalse(); + prices.HasVolume.ShouldBeFalse(); + } + + [Fact] + public void ToBarAlignedArrayPadsColdBarsWithNaNAndNeverZero() + { + // Arrange + // SMA(3) over closes 1..10: bar 2 is the mean of 1, 2, 3 = 2.0 and every later bar k is + // the mean of k - 1, k, k + 1 = k, so bar 9 reads 9.0. + // The day a[FirstBar - 1] becomes 0.0 instead of NaN is the day the whole bug returns: + // 0.0 is a plausible-looking number, NaN is not. + PriceSeries prices = PriceSeries.FromClose(Ramp(10)); + IndicatorSeries sma = prices.Sma(3); + + // Act + double[] aligned = sma.ToBarAlignedArray(); + + // Assert + sma.FirstBar.ShouldBe(2); + sma.LastBar.ShouldBe(9); + sma.WarmCount.ShouldBe(8); + sma.Latest.ShouldBe(9.0); + sma[2].ShouldBe(2.0); + + aligned.Length.ShouldBe(10); + double.IsNaN(aligned[0]).ShouldBeTrue(); + double.IsNaN(aligned[1]).ShouldBeTrue(); + aligned[2].ShouldBe(2.0); + aligned[9].ShouldBe(9.0); + } + + [Fact] + public void ToBarAlignedArrayOfANotYetWarmSeriesIsAllNaN() + { + // Arrange + // Five closes and a thirty-period SMA: no bar is warm, so every bar is NaN and none is 0.0. + PriceSeries prices = PriceSeries.FromClose([1.0, 2.0, 3.0, 4.0, 5.0]); + IndicatorSeries sma = prices.Sma(30); + + // Act + double[] aligned = sma.ToBarAlignedArray(); + + // Assert + aligned.Length.ShouldBe(5); + foreach (double value in aligned) + { + double.IsNaN(value).ShouldBeTrue(); + } + } + + [Fact] + public void CopyBarAlignedWritesByBarIndexAndGuardsTheDestination() + { + // Arrange + // SMA(30) over closes 1..100: bar 28 is cold (NaN), bar 29 is 15.5, bar 99 is 85.5. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + IndicatorSeries sma = prices.Sma(30); + double[] exact = new double[100]; + double[] overlong = new double[120]; + Array.Fill(overlong, -1.0); + + // Act + sma.CopyBarAligned(exact); + sma.CopyBarAligned(overlong); + ArgumentException tooSmall = Should.Throw(() => + { + sma.CopyBarAligned(new double[10]); + }); + + // Assert + double.IsNaN(exact[28]).ShouldBeTrue(); + exact[29].ShouldBe(15.5); + exact[99].ShouldBe(85.5); + tooSmall.ShouldBeOfType(); + tooSmall.ParamName.ShouldBe("destination"); + + // Everything past BarCount is left untouched. + overlong[99].ShouldBe(85.5); + overlong[100].ShouldBe(-1.0); + overlong[119].ShouldBe(-1.0); + } + + [Fact] + public void EnumerationYieldsWarmBarsInAscendingBarOrder() + { + // Arrange + // SMA(3) over closes 1..10 is warm on bars 2..9 with value == bar, so the pairs are + // (2,2) (3,3) (4,4) (5,5) (6,6) (7,7) (8,8) (9,9). The Bar component is a BAR index, + // not an array index -- an implementation that yielded 0..7 would fail here. + PriceSeries prices = PriceSeries.FromClose(Ramp(10)); + IndicatorSeries sma = prices.Sma(3); + IndicatorSeries notWarm = PriceSeries.FromClose([1.0, 2.0, 3.0, 4.0, 5.0]).Sma(30); + List<(int Bar, double Value)> observed = []; + int emptyIterations = 0; + + // Act + foreach ((int bar, double value) in sma) + { + observed.Add((bar, value)); + } + + foreach ((int _, double _) in notWarm) + { + emptyIterations++; + } + + // Assert + observed.Count.ShouldBe(8); + observed[0].ShouldBe((2, 2.0)); + observed[1].ShouldBe((3, 3.0)); + observed[2].ShouldBe((4, 4.0)); + observed[3].ShouldBe((5, 5.0)); + observed[4].ShouldBe((6, 6.0)); + observed[5].ShouldBe((7, 7.0)); + observed[6].ShouldBe((8, 8.0)); + observed[7].ShouldBe((9, 9.0)); + emptyIterations.ShouldBe(0); + } + + [Fact] + public void CurrentBeforeTheFirstMoveNextOrAfterTheLastReportsMisuseRatherThanCrashing() + { + // Arrange + // Enumerator is a public type, so hand-driving it is reachable. Reading Current out of + // position used to throw NullReferenceException on a series with no values and + // IndexOutOfRangeException on one with values -- both of which read as a library defect + // rather than as caller misuse. foreach never reaches this state, which is why nothing + // caught it. + PriceSeries prices = PriceSeries.FromClose(Ramp(10)); + IndicatorSeries sma = prices.Sma(3); + IndicatorSeries.Enumerator beforeStart = sma.GetEnumerator(); + IndicatorSeries.Enumerator onNothing = IndicatorSeries.Empty(100).GetEnumerator(); + IndicatorSeries.Enumerator exhausted = sma.GetEnumerator(); + while (exhausted.MoveNext()) + { + // Drain it. + } + + // Act + InvalidOperationException notStarted = Should.Throw(() => + { + _ = beforeStart.Current; + }); + + InvalidOperationException noValues = Should.Throw(() => + { + _ = onNothing.Current; + }); + + InvalidOperationException finished = Should.Throw(() => + { + _ = exhausted.Current; + }); + + // Assert + notStarted.ShouldBeOfType(); + noValues.ShouldBeOfType(); + finished.ShouldBeOfType(); + onNothing.MoveNext().ShouldBeFalse(); + exhausted.MoveNext().ShouldBeFalse(); + } + + [Fact] + public void ToBarAlignedNullableArrayReportsAbsenceAsNullRatherThanAsASentinel() + { + // Arrange + // ToBarAlignedArray has to pad with NaN because a double[] cannot hold null, which collides + // with a computed non-finite value. The nullable projection has no sentinel at all, so the + // two states stay distinguishable. + PriceSeries prices = PriceSeries.FromClose(Ramp(10)); + IndicatorSeries sma = prices.Sma(3); + + // Act + double?[] aligned = sma.ToBarAlignedNullableArray(); + double?[] nothing = PriceSeries.FromClose([1.0, 2.0, 3.0]).Sma(30).ToBarAlignedNullableArray(); + + // Assert + aligned.Length.ShouldBe(10); + aligned[0].ShouldBeNull(); + aligned[1].ShouldBeNull(); + aligned[2].ShouldBe(2.0); + aligned[9].ShouldBe(9.0); + + nothing.Length.ShouldBe(3); + foreach (double? value in nothing) + { + value.ShouldBeNull(); + } + } + + [Fact] + public void WarmValuesToArrayLetsTheValuesEscapeTheRefStructSpan() + { + // Arrange + // WarmValues is a ReadOnlySpan, so it cannot cross into a LINQ query, an async method or a + // field. Without this member there is no non-sentinel way to get the values out at all. + PriceSeries prices = PriceSeries.FromClose(Ramp(10)); + IndicatorSeries sma = prices.Sma(3); + + // Act + double[] values = sma.WarmValuesToArray(); + double[] none = PriceSeries.FromClose([1.0, 2.0, 3.0]).Sma(30).WarmValuesToArray(); + double[] narrowed = sma.AsOf(5).WarmValuesToArray(); + + // Assert + values.Length.ShouldBe(8); + values[0].ShouldBe(2.0); + values[7].ShouldBe(9.0); + values.Max().ShouldBe(9.0); + none.Length.ShouldBe(0); + + // AsOf keeps the same backing array but only four values survive, so the copy must be + // sliced to WarmCount rather than handed the whole array. + narrowed.Length.ShouldBe(4); + narrowed[3].ShouldBe(5.0); + } + + [Fact] + public void CreateCopiesSoTheCallerKeepsSoleOwnershipOfItsArray() + { + // Arrange + // The public entry point cannot know whether the array it is handed is reachable from + // anywhere else -- result.Real is a live public property -- so immutability can only be + // unconditional if it copies. + double[] values = [1.0, 2.0, 3.0, 0.0, 0.0]; + + // Act + IndicatorSeries series = IndicatorSeries.Create(RetCode.Success, begIdx: 2, nbElement: 3, values, barCount: 5); + values[0] = 999.0; + values[2] = 999.0; + + // Assert + series[2].ShouldBe(1.0); + series[4].ShouldBe(3.0); + series.Latest.ShouldBe(3.0); + series.WarmValues[0].ShouldBe(1.0); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/MomentumIndicatorsTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/MomentumIndicatorsTests.cs new file mode 100644 index 00000000..852e5056 --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/MomentumIndicatorsTests.cs @@ -0,0 +1,349 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// Value-asserting tests for the momentum studies. Each fixture is chosen so the correct answer is a +/// closed form that can be derived on paper rather than copied out of a previous run. +/// +public class MomentumIndicatorsTests +{ + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + private static double[] Constant(int count, double value) + { + double[] values = new double[count]; + Array.Fill(values, value); + return values; + } + + private static PriceSeries ConstantRangeHlc(int barCount) + { + // high 102, low 98, close 100 on every bar: the close is exactly midway in a range that + // never moves, so %K = (100 - 98) / ((102 - 98) / 100) = 50 on every bar. + return PriceSeries.FromHlc(Constant(barCount, 102.0), Constant(barCount, 98.0), Constant(barCount, 100.0)); + } + + [Fact] + public void RsiSaturatesAtOneHundredOnAStrictlyIncreasingSeries() + { + // Arrange + // Closes 1..100 rise by exactly 1 every bar, so Wilder's average loss is 0 on every warm + // bar and RSI = 100 * gain / (gain + 0) = 100.0 exactly. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + IndicatorSeries rsi = prices.Rsi(14); + + // Assert + rsi.Latest.ShouldBe(100.0); + rsi.FirstBar.ShouldBe(14); + rsi.WarmCount.ShouldBe(86); + rsi[14].ShouldBe(100.0); + } + + [Fact] + public void RsiOnAFlatSeriesIsZeroAndNotNaN() + { + // Arrange + // Fifty identical closes: average gain and average loss are both 0, so the ratio is 0/0. + // TA-Lib C returns 0 in that state; the unguarded division used to return NaN, and a NaN + // launders itself into every downstream comparison. + PriceSeries prices = PriceSeries.FromClose(Constant(50, 100.0)); + + // Act + IndicatorSeries rsi = prices.Rsi(14); + + // Assert + rsi.Latest.ShouldBe(0.0); + double.IsNaN(rsi.Latest!.Value).ShouldBeFalse(); + rsi.FirstBar.ShouldBe(14); + rsi.WarmCount.ShouldBe(36); + } + + [Fact] + public void AnAbsentRsiAssertsNothingInEitherDirection() + { + // Arrange + // Five closes and a fourteen-period RSI: there is no value at all. This is the direct + // counter-test to the abandoned branch, where the corrupted read produced 0.0 and made + // IsOversold true on every input. A null compares false against BOTH thresholds. + PriceSeries prices = PriceSeries.FromClose([1.0, 2.0, 3.0, 4.0, 5.0]); + + // Act + IndicatorSeries rsi = prices.Rsi(14); + double? latest = rsi.Latest; + + // Assert + latest.ShouldBeNull(); + (latest < 30.0).ShouldBeFalse(); + (latest > 70.0).ShouldBeFalse(); + } + + [Fact] + public void MacdLineOnAUnitSlopeRampIsTheDifferenceOfTheTwoEmaLags() + { + // Arrange + // On a ramp of slope 1 an N-period EMA seeded with the mean of the first N closes sits + // exactly (N - 1) / 2 below the price and stays there. So + // EMA(12) - EMA(26) = (price - 5.5) - (price - 12.5) = 7.0 + // on every warm bar, and the recursion is exact in binary floating point because the seed + // already equals the steady state. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + MacdSeries macd = prices.Macd(); + + // Assert + macd.Line.Latest.ShouldBe(7.0); + } + + [Fact] + public void MacdSignalIsTheEmaOfAConstantLineAndTheHistogramCancels() + { + // Arrange + // The MACD line is identically 7.0, so its 9-period EMA is 7.0 and the histogram, + // line minus signal, is exactly 0.0. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + MacdSeries macd = prices.Macd(); + + // Assert + macd.Signal.Latest.ShouldBe(7.0); + macd.Histogram.Latest.ShouldBe(0.0); + } + + [Fact] + public void MacdComponentsShareTheCombinedLookbackAlignment() + { + // Arrange + // The lookback is (slowPeriod - 1) + (signalPeriod - 1) = 25 + 8 = 33, so the first warm + // bar is 33 and 100 - 33 = 67 bars carry a value. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + MacdSeries macd = prices.Macd(); + + // Assert + macd.Line.FirstBar.ShouldBe(33); + macd.Line.WarmCount.ShouldBe(67); + macd.Line[33].ShouldBe(7.0); + macd.Signal.FirstBar.ShouldBe(33); + macd.Histogram.FirstBar.ShouldBe(33); + macd.Line[32].ShouldBeNull(); + } + + [Fact] + public void StochOnAFlatRangeSitsExactlyMidwayAtFifty() + { + // Arrange + // Sixty bars with high 102, low 98, close 100. The raw %K is + // (close - lowest) / ((highest - lowest) / 100) = 2 / 0.04 = 50, and a simple moving + // average of a constant 50 is 50, twice over. + PriceSeries prices = ConstantRangeHlc(60); + + // Act + StochSeries stoch = prices.Stoch(); + + // Assert + stoch.SlowK.Latest.ShouldBe(50.0); + stoch.SlowD.Latest.ShouldBe(50.0); + } + + [Fact] + public void StochComponentsShareTheCombinedLookbackAlignment() + { + // Arrange + // The lookback is (fastK - 1) + (slowK - 1) + (slowD - 1) = 4 + 2 + 2 = 8, so the first + // warm bar is 8 and 60 - 8 = 52 bars carry a value. + PriceSeries prices = ConstantRangeHlc(60); + + // Act + StochSeries stoch = prices.Stoch(); + + // Assert + stoch.SlowK.FirstBar.ShouldBe(8); + stoch.SlowK.WarmCount.ShouldBe(52); + stoch.SlowK.BarCount.ShouldBe(60); + stoch.SlowD.FirstBar.ShouldBe(8); + stoch.SlowD.WarmCount.ShouldBe(52); + stoch.SlowK[7].ShouldBeNull(); + stoch.SlowK[8].ShouldBe(50.0); + } + + [Fact] + public void MultiOutputWrappersDeconstructPositionallyIntoAddressableSeries() + { + // Arrange + PriceSeries closes = PriceSeries.FromClose(Ramp(100)); + PriceSeries hlc = ConstantRangeHlc(60); + + // Act + (IndicatorSeries line, IndicatorSeries signal, IndicatorSeries histogram) = closes.Macd(); + (IndicatorSeries upper, IndicatorSeries middle, IndicatorSeries lower) = closes.BollingerBands(20, 2.0, 2.0); + (IndicatorSeries slowK, IndicatorSeries slowD) = hlc.Stoch(); + + // Assert + line[33].ShouldBe(7.0); + signal[33].ShouldBe(7.0); + histogram[33].ShouldBe(0.0); + + middle[19].ShouldBe(10.5); + upper[19].ShouldBe(10.5 + (2.0 * Math.Sqrt(33.25))); + lower[19].ShouldBe(10.5 - (2.0 * Math.Sqrt(33.25))); + + slowK[8].ShouldBe(50.0); + slowD[8].ShouldBe(50.0); + } + + [Fact] + public void AMacdLineSignalCrossIsExpressedWithoutAnyThresholdPredicate() + { + // Arrange + // On the ramp the line and the signal are both identically 7.0, so the line is never + // strictly above the signal and there is never a crossing. That the expression compiles + // and answers sensibly at every bar is the point: no IsBullish is required. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + MacdSeries macd = prices.Macd(); + + // Act + bool anyCrossing = false; + for (int bar = 0; bar < macd.Line.BarCount; bar++) + { + if (macd.Line.CrossedAbove(macd.Signal, bar)) + { + anyCrossing = true; + } + } + + // Assert + macd.Line.BarCount.ShouldBe(100); + anyCrossing.ShouldBeFalse(); + } + + [Fact] + public void RsiFallsGeometricallyOnceTheSeriesTurnsSoEveryWarmBarHasItsOwnAnswer() + { + // Arrange + // Closes rise by 1 for bars 0..14 and fall by 1 for ever afterwards. The seed at bar 14 is + // avgGain = 1 and avgLoss = 0, so RSI is 100 there. Each falling bar multiplies avgGain by + // 13/14 and, because avgGain + avgLoss stays exactly 1, RSI(14 + k) = 100 * (13/14)^k. + // Every warm bar therefore carries a different number and a one-bar shift changes all of + // them -- unlike the plain ramp, on which RSI is pinned at 100 and a shift is invisible. + double[] closes = new double[100]; + for (int bar = 0; bar < 100; bar++) + { + closes[bar] = bar <= 14 ? 1.0 + bar : 15.0 - (bar - 14); + } + + PriceSeries prices = PriceSeries.FromClose(closes); + + // Act + IndicatorSeries rsi = prices.Rsi(14); + + // Assert + rsi.FirstBar.ShouldBe(14); + rsi.WarmCount.ShouldBe(86); + rsi[14].Value.ShouldBe(100.0, 1e-9); + rsi[15].Value.ShouldBe(92.85714285714286, 1e-9); + rsi[16].Value.ShouldBe(86.22448979591837, 1e-9); + rsi[24].Value.ShouldBe(47.659904336004004, 1e-9); + rsi[54].Value.ShouldBe(5.159559975746875, 1e-9); + rsi[99].Value.ShouldBe(0.1837815514462741, 1e-9); + rsi[13].ShouldBeNull(); + + // Strictly decreasing after the turn: no two warm bars agree, so nothing here is masked. + for (int bar = 16; bar <= 99; bar++) + { + rsi[bar].Value.ShouldBeLessThan(rsi[bar - 1].Value); + } + } + + [Fact] + public void MacdLineDecaysAnalyticallyOnceTheRampFlattens() + { + // Arrange + // closes[bar] = 1 + bar below 60, so the last RISING bar is 59 (which is already 60.0) and + // price is flat from there on. On the ramp each N-period EMA sits exactly (N - 1) / 2 below + // price, so at bar 59 the fast EMA is 60 - 5.5 and the slow one 60 - 12.5, giving a line of + // exactly 7. Once price stops moving each EMA decays towards 60 by its own factor + // 1 - 2 / (N + 1), so + // line(59 + m) = 12.5 * (25/27)^m - 5.5 * (11/13)^m + // Note the anchor is bar 59, not 60: bar 60 is the first FLAT bar, so one decay step has + // already been applied there and line(60) is 6.9202..., not 7. + double[] closes = new double[100]; + for (int bar = 0; bar < 100; bar++) + { + closes[bar] = bar < 60 ? 1.0 + bar : 60.0; + } + + PriceSeries prices = PriceSeries.FromClose(closes); + + // Act + MacdSeries macd = prices.Macd(); + + // Assert + macd.Line.FirstBar.ShouldBe(33); + macd.Line[59].Value.ShouldBe(7.0, 1e-9); + macd.Line[60].Value.ShouldBe(6.92022792022792, 1e-9); + macd.Line[61].Value.ShouldBe(6.778865431287085, 1e-9); + macd.Line[69].Value.ShouldBe(4.755119558996247, 1e-9); + macd.Line[98].Value.ShouldBe(0.6132726824101662, 1e-9); + } + + [Fact] + public void StochTracksTheClosePositionBarByBarAndDisagreesWhenHighAndLowAreSwapped() + { + // Arrange + // A fixed band -- high 110 and low 100 on every bar -- with the close sawing between 100 and + // 109. The window's highest and lowest are therefore always 110 and 100, so the raw %K is + // 10 * (bar % 10), the slow %K is its three-bar mean and the slow %D is the mean of that. + // Every consecutive pair of warm bars differs, which the flat-range fixture cannot show: + // there %K is 50 everywhere and swapping the high and the low arguments produces a byte- + // identical result. + double[] high = Constant(60, 110.0); + double[] low = Constant(60, 100.0); + double[] close = new double[60]; + for (int bar = 0; bar < 60; bar++) + { + close[bar] = 100.0 + (bar % 10); + } + + PriceSeries prices = PriceSeries.FromHlc(high, low, close); + PriceSeries swapped = PriceSeries.FromHlc(low, high, close); + + // Act + StochSeries stoch = prices.Stoch(); + StochSeries wrongWayRound = swapped.Stoch(); + + // Assert + stoch.SlowK.FirstBar.ShouldBe(8); + stoch.SlowK[8].Value.ShouldBe(70.0, 1e-9); + stoch.SlowD[8].Value.ShouldBe(60.0, 1e-9); + stoch.SlowK[20].Value.ShouldBe(56.666666666666664, 1e-9); + stoch.SlowK[21].Value.ShouldBe(33.333333333333336, 1e-9); + stoch.SlowD[20].Value.ShouldBe(68.88888888888889, 1e-9); + stoch.SlowK[59].Value.ShouldBe(80.0, 1e-9); + + // Consecutive bars differ, so an off-by-one could not hide in this fixture. + stoch.SlowK[20].ShouldNotBe(stoch.SlowK[21]); + + // The whole point: reversing the two range arguments must change the answer. + wrongWayRound.SlowK[20].ShouldNotBe(stoch.SlowK[20]); + wrongWayRound.SlowK[8].ShouldNotBe(stoch.SlowK[8]); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/OverlapStudyIndicatorsTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/OverlapStudyIndicatorsTests.cs new file mode 100644 index 00000000..3fbc39ec --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/OverlapStudyIndicatorsTests.cs @@ -0,0 +1,368 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// Value-asserting tests for the overlap studies, plus the two cross-cutting rules that live here by +/// the specification's file assignment: every default is TA-Lib's verbatim, and every indicator over +/// an empty price series returns an empty series rather than throwing. +/// +public class OverlapStudyIndicatorsTests +{ + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + private static double[] Constant(int count, double value) + { + double[] values = new double[count]; + Array.Fill(values, value); + return values; + } + + private static PriceSeries ConstantRangeHlc(int barCount) + { + return PriceSeries.FromHlc(Constant(barCount, 102.0), Constant(barCount, 98.0), Constant(barCount, 100.0)); + } + + // Equality on IndicatorSeries is reference identity over the backing array, so two independently + // computed series are never equal even when every value agrees. Comparing them therefore has to + // compare shape and every bar's value. + private static void ShouldMatch(IndicatorSeries expected, IndicatorSeries actual) + { + actual.RetCode.ShouldBe(expected.RetCode); + actual.BarCount.ShouldBe(expected.BarCount); + actual.FirstBar.ShouldBe(expected.FirstBar); + actual.LastBar.ShouldBe(expected.LastBar); + actual.WarmCount.ShouldBe(expected.WarmCount); + + for (int bar = 0; bar < expected.BarCount; bar++) + { + actual[bar].ShouldBe(expected[bar], $"Bar {bar} differs."); + } + } + + private static void ShouldBeEmptyWithNoBars(IndicatorSeries series) + { + series.HasValues.ShouldBeFalse(); + series.BarCount.ShouldBe(0); + series.RetCode.ShouldBe(RetCode.Success); + series.ToBarAlignedArray().Length.ShouldBe(0); + } + + [Fact] + public void SmaAtTheExactLowerBoundaryProducesOneValueAndOneFewerBarProducesNone() + { + // Arrange + // Thirty closes 1..30 is the shortest series a 30-period SMA can warm up on: exactly one + // value, the mean of 1..30 = 15.5, landing on bar 29. + PriceSeries exactly = PriceSeries.FromClose(Ramp(30)); + PriceSeries oneShort = PriceSeries.FromClose(Ramp(29)); + + // Act + IndicatorSeries warm = exactly.Sma(30); + IndicatorSeries cold = oneShort.Sma(30); + + // Assert + warm.WarmCount.ShouldBe(1); + warm.FirstBar.ShouldBe(29); + warm.LastBar.ShouldBe(29); + warm.Latest.ShouldBe(15.5); + warm[28].ShouldBeNull(); + cold.HasValues.ShouldBeFalse(); + } + + [Fact] + public void SmaWithTheSmallestLegalPeriod() + { + // Arrange + // SMA(2) over [1, 2, 3]: bar 1 = (1 + 2) / 2 = 1.5, bar 2 = (2 + 3) / 2 = 2.5. + PriceSeries prices = PriceSeries.FromClose([1.0, 2.0, 3.0]); + + // Act + IndicatorSeries sma = prices.Sma(2); + + // Assert + sma.FirstBar.ShouldBe(1); + sma.WarmCount.ShouldBe(2); + sma[0].ShouldBeNull(); + sma[1].ShouldBe(1.5); + sma[2].ShouldBe(2.5); + } + + [Fact] + public void EmaOverAConstantSeriesEqualsTheConstantExactly() + { + // Arrange + // An exponential average of a constant must be that constant: the seed is the mean of the + // first twenty 100s and every later bar is 100 + (100 - 100) * k. Anything lower means the + // seeding loop dropped a term, which is the TA_INT_EMA defect already fixed on this branch. + PriceSeries prices = PriceSeries.FromClose(Constant(100, 100.0)); + + // Act + IndicatorSeries ema = prices.Ema(20); + + // Assert + ema.Latest.ShouldBe(100.0); + ema.FirstBar.ShouldBe(19); + ema.WarmCount.ShouldBe(81); + foreach ((int _, double value) in ema) + { + value.ShouldBe(100.0); + } + } + + [Fact] + public void BollingerBandsMiddleBandIsTheSimpleMovingAverage() + { + // Arrange + // Closes 1..100 with a 20-period window. Middle at bar 99 is the mean of closes 81..100 + // = (81 + 100) / 2 = 90.5; at bar 19 it is the mean of closes 1..20 = 10.5. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + BollingerBandsSeries bands = prices.BollingerBands(20, 2.0, 2.0); + + // Assert + bands.Middle.Latest.ShouldBe(90.5); + bands.Middle.FirstBar.ShouldBe(19); + bands.Middle.WarmCount.ShouldBe(81); + bands.Middle[19].ShouldBe(10.5); + } + + [Fact] + public void BollingerBandsEnvelopeIsTwoPopulationStandardDeviationsWide() + { + // Arrange + // The population standard deviation of n consecutive integers is sqrt((n^2 - 1) / 12); + // for n = 20 that is sqrt(399 / 12) = sqrt(33.25) = 5.766281297335398. + // Every intermediate value here is exactly representable -- the running sum of squares for + // closes 81..100 is 164470, 164470 / 20 = 8223.5, 90.5 * 90.5 = 8190.25 and the difference + // is exactly 33.25 -- so exact equality is the right assertion, not a tolerance. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + BollingerBandsSeries bands = prices.BollingerBands(20, 2.0, 2.0); + + // Assert + bands.Upper.Latest.ShouldBe(90.5 + (2.0 * Math.Sqrt(33.25))); + bands.Lower.Latest.ShouldBe(90.5 - (2.0 * Math.Sqrt(33.25))); + } + + [Fact] + public void AllThreeBollingerComponentsAreIndependentlyValidAndShareTheirAlignment() + { + // Arrange + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + BollingerBandsSeries bands = prices.BollingerBands(20, 2.0, 2.0); + (IndicatorSeries upper, IndicatorSeries middle, IndicatorSeries lower) = bands; + + // Assert + upper.FirstBar.ShouldBe(19); + middle.FirstBar.ShouldBe(19); + lower.FirstBar.ShouldBe(19); + upper.WarmCount.ShouldBe(81); + middle.WarmCount.ShouldBe(81); + lower.WarmCount.ShouldBe(81); + upper.BarCount.ShouldBe(100); + middle.BarCount.ShouldBe(100); + lower.BarCount.ShouldBe(100); + + // Each is independently addressable by bar index and the envelope brackets the middle. + upper[19]!.Value.ShouldBeGreaterThan(middle[19]!.Value); + lower[19]!.Value.ShouldBeLessThan(middle[19]!.Value); + upper[99]!.Value.ShouldBeGreaterThan(middle[99]!.Value); + } + + [Fact] + public void BollingerBandsDefaultPeriodIsTaLibsFiveAndNotTheTaughtTwenty() + { + // Arrange + // TA-Lib's BBANDS default is 5 even though 20 is the conventional trading choice. The rule + // is that the fluent layer never invents a number, so this test exists specifically to stop + // the default silently drifting to 20. + // With period 5 over closes 1..100: bar 4's mean is (1 + 5) / 2 = 3.0 and the population + // standard deviation of 1..5 is sqrt(55/5 - 9) = sqrt(2), so Upper[4] = 3 + 2 * sqrt(2). + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + BollingerBandsSeries defaulted = prices.BollingerBands(); + BollingerBandsSeries explicitly = prices.BollingerBands(5, 2.0, 2.0, MAType.Sma); + + // Assert + defaulted.Middle.FirstBar.ShouldBe(4); + defaulted.Middle[4].ShouldBe(3.0); + defaulted.Upper[4].ShouldBe(3.0 + (2.0 * Math.Sqrt(2.0))); + ShouldMatch(explicitly.Upper, defaulted.Upper); + ShouldMatch(explicitly.Middle, defaulted.Middle); + ShouldMatch(explicitly.Lower, defaulted.Lower); + } + + [Fact] + public void EveryIndicatorDefaultIsCopiedVerbatimFromTaMath() + { + // Arrange + // The Stoch case is the one that matters most: the abandoned branch silently redefined + // fastKPeriod from TA-Lib's 5 to 14, which changes every value it ever produced. + PriceSeries closes = PriceSeries.FromClose(Ramp(100)); + PriceSeries hlc = ConstantRangeHlc(60); + + // Act + IndicatorSeries sma = closes.Sma(); + IndicatorSeries ema = closes.Ema(); + IndicatorSeries rsi = closes.Rsi(); + IndicatorSeries atr = hlc.Atr(); + MacdSeries macd = closes.Macd(); + StochSeries stoch = hlc.Stoch(); + + // Assert + ShouldMatch(closes.Sma(30), sma); + ShouldMatch(closes.Ema(30), ema); + ShouldMatch(closes.Rsi(14), rsi); + ShouldMatch(hlc.Atr(14), atr); + + MacdSeries explicitMacd = closes.Macd(12, 26, 9); + ShouldMatch(explicitMacd.Line, macd.Line); + ShouldMatch(explicitMacd.Signal, macd.Signal); + ShouldMatch(explicitMacd.Histogram, macd.Histogram); + + StochSeries explicitStoch = hlc.Stoch(5, 3, MAType.Sma, 3, MAType.Sma); + ShouldMatch(explicitStoch.SlowK, stoch.SlowK); + ShouldMatch(explicitStoch.SlowD, stoch.SlowD); + } + + [Fact] + public void EveryIndicatorOverAnEmptyPriceSeriesReturnsAnEmptySeries() + { + // Arrange + // TAMath is never called for an empty series, because endIdx would be -1 and that returns + // OutOfRangeEndIndex -- not a state worth propagating. + // Atr and Stoch need high/low, and the availability guard runs before the emptiness + // short-circuit, so their fixture is an empty high/low/close feed rather than default. + PriceSeries emptyClose = PriceSeries.Empty; + PriceSeries emptyHlc = PriceSeries.FromHlc([], [], []); + + // Act + IndicatorSeries sma = emptyClose.Sma(30); + IndicatorSeries ema = emptyClose.Ema(30); + IndicatorSeries rsi = emptyClose.Rsi(14); + MacdSeries macd = emptyClose.Macd(); + BollingerBandsSeries bands = emptyClose.BollingerBands(); + IndicatorSeries atr = emptyHlc.Atr(14); + StochSeries stoch = emptyHlc.Stoch(); + + // Assert + emptyHlc.HasHighLow.ShouldBeTrue(); + + ShouldBeEmptyWithNoBars(sma); + ShouldBeEmptyWithNoBars(ema); + ShouldBeEmptyWithNoBars(rsi); + ShouldBeEmptyWithNoBars(atr); + ShouldBeEmptyWithNoBars(macd.Line); + ShouldBeEmptyWithNoBars(macd.Signal); + ShouldBeEmptyWithNoBars(macd.Histogram); + ShouldBeEmptyWithNoBars(bands.Upper); + ShouldBeEmptyWithNoBars(bands.Middle); + ShouldBeEmptyWithNoBars(bands.Lower); + ShouldBeEmptyWithNoBars(stoch.SlowK); + ShouldBeEmptyWithNoBars(stoch.SlowD); + } + + [Fact] + public void APeriodLargerThanTheDataIsNotAnError() + { + // Arrange + // Three closes and a thirty-period SMA is a perfectly ordinary warm-up state, so it returns + // an empty series with Success rather than throwing or reporting a bad parameter. + PriceSeries prices = PriceSeries.FromClose([1.0, 2.0, 3.0]); + + // Act + IndicatorSeries sma = prices.Sma(30); + + // Assert + sma.HasValues.ShouldBeFalse(); + sma.RetCode.ShouldBe(RetCode.Success); + sma.BarCount.ShouldBe(3); + sma.FirstBar.ShouldBeNull(); + sma.Latest.ShouldBeNull(); + } + + [Fact] + public void EmaTrailsARampByHalfTheLookbackSoEveryWarmBarHasItsOwnAnswer() + { + // Arrange + // On a slope-1 ramp an N-period EMA seeded with the mean of the first N closes is already + // at its steady state, sitting exactly (N - 1) / 2 below price and staying there. For + // N = 20 that is price - 9.5 on every warm bar, which is a different number on every bar -- + // unlike the constant fixture, where EMA is 100 everywhere and a shift is invisible. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + IndicatorSeries ema = prices.Ema(20); + + // Assert + ema.FirstBar.ShouldBe(19); + ema.WarmCount.ShouldBe(81); + ema[19].Value.ShouldBe(10.5, 1e-9); + ema[50].Value.ShouldBe(41.5, 1e-9); + ema[99].Value.ShouldBe(90.5, 1e-9); + ema[18].ShouldBeNull(); + + foreach ((int bar, double value) in ema) + { + value.ShouldBe(bar + 1.0 - 9.5, 1e-9, $"EMA was {value} at bar {bar}."); + } + } + + [Fact] + public void PriceSeriesEmptyCarriesNoComponentsSoTheRangeIndicatorsRefuseIt() + { + // Arrange + // PriceSeries.Empty is default(PriceSeries), so it carries no high, low or volume at all and + // the component guard -- which runs before the emptiness short-circuit -- refuses. This is + // the documented behaviour rather than an accident, and the component-agnostic empty feed + // is FromOhlcv with five empty spans. + PriceSeries none = PriceSeries.Empty; + PriceSeries emptyOhlcv = PriceSeries.FromOhlcv([], [], [], [], []); + + // Act + // (the refusals are asserted below) + + // Assert + none.HasHighLow.ShouldBeFalse(); + none.HasVolume.ShouldBeFalse(); + + Should.Throw(() => { _ = none.Atr(14); }); + Should.Throw(() => { _ = none.Stoch(); }); + Should.Throw(() => { _ = none.Adx(14); }); + Should.Throw(() => { _ = none.Obv(); }); + + // The close-only indicators are happy with it. + ShouldBeEmptyWithNoBars(none.Sma(30)); + ShouldBeEmptyWithNoBars(none.Ema(30)); + ShouldBeEmptyWithNoBars(none.Rsi(14)); + ShouldBeEmptyWithNoBars(none.Macd().Line); + ShouldBeEmptyWithNoBars(none.BollingerBands().Middle); + + // And the fully-populated empty feed answers every one of them. + emptyOhlcv.HasHighLow.ShouldBeTrue(); + emptyOhlcv.HasVolume.ShouldBeTrue(); + ShouldBeEmptyWithNoBars(emptyOhlcv.Atr(14)); + ShouldBeEmptyWithNoBars(emptyOhlcv.Adx(14)); + ShouldBeEmptyWithNoBars(emptyOhlcv.Obv()); + ShouldBeEmptyWithNoBars(emptyOhlcv.Stoch().SlowK); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/PriceSeriesTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/PriceSeriesTests.cs new file mode 100644 index 00000000..ab013eef --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/PriceSeriesTests.cs @@ -0,0 +1,444 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +using System.Reflection; + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// The entry point. It owns the copy (so immutability is unconditional rather than promised), the +/// ragged-length rejection, the component-availability rules, and the Align escape hatch that +/// bar-aligns the ninety-odd indicators the fluent surface does not wrap. +/// +public class PriceSeriesTests +{ + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + // Spans cannot be captured by a lambda, so the component accessors are exercised through these. + private static int OpenLength(PriceSeries prices) + { + return prices.Open.Length; + } + + private static int HighLength(PriceSeries prices) + { + return prices.High.Length; + } + + private static int LowLength(PriceSeries prices) + { + return prices.Low.Length; + } + + private static int VolumeLength(PriceSeries prices) + { + return prices.Volume.Length; + } + + [Fact] + public void FactoriesCopyTheirInputSoLaterMutationCannotBeObserved() + { + // Arrange + // SMA(2) over [10, 20, 30, 40] is 15, 25, 35 on bars 1..3. If the factory kept a reference, + // mutating element 0 to 1000 after construction would make bar 1 read (1000 + 20) / 2 = 510. + double[] closes = [10.0, 20.0, 30.0, 40.0]; + PriceSeries prices = PriceSeries.FromClose(closes); + + // Act + closes[0] = 1000.0; + IndicatorSeries sma = prices.Sma(2); + + // Assert + prices.Close[0].ShouldBe(10.0); + sma[1].ShouldBe(15.0); + sma[2].ShouldBe(25.0); + sma[3].ShouldBe(35.0); + } + + [Fact] + public void RaggedComponentArraysAreRejectedNamingTheOffendingParameter() + { + // Arrange + // close defines the length; every other component must match it, checked in the order + // open, high, low, volume. Without this guard TAMath.Atr throws IndexOutOfRangeException + // from deep inside TAFunc. Only the exception type and ParamName are contractual. + double[] hundred = new double[100]; + + // Act + ArgumentException shortHigh = Should.Throw(() => + { + _ = PriceSeries.FromHlc(new double[50], hundred, hundred); + }); + + ArgumentException shortOpen = Should.Throw(() => + { + _ = PriceSeries.FromOhlc(new double[50], hundred, hundred, hundred); + }); + + ArgumentException shortLow = Should.Throw(() => + { + _ = PriceSeries.FromOhlc(hundred, hundred, new double[50], hundred); + }); + + // Assert + shortHigh.ShouldBeOfType(); + shortHigh.ParamName.ShouldBe("high"); + shortOpen.ShouldBeOfType(); + shortOpen.ParamName.ShouldBe("open"); + shortLow.ShouldBeOfType(); + shortLow.ParamName.ShouldBe("low"); + } + + [Fact] + public void AnEmptyFeedIsALegalPriceSeries() + { + // Arrange + // A zero-length feed is a normal state for a streaming engine; throwing here would force + // defensive code at every call site. + double[] nothing = []; + + // Act + PriceSeries prices = PriceSeries.FromClose(nothing); + + // Assert + prices.BarCount.ShouldBe(0); + prices.IsEmpty.ShouldBeTrue(); + prices.Close.Length.ShouldBe(0); + } + + [Fact] + public void ComponentAvailabilityReflectsTheFactoryThatWasUsed() + { + // Arrange + double[] bars = Ramp(10); + + // Act + PriceSeries close = PriceSeries.FromClose(bars); + PriceSeries hlc = PriceSeries.FromHlc(bars, bars, bars); + PriceSeries ohlc = PriceSeries.FromOhlc(bars, bars, bars, bars); + PriceSeries ohlcv = PriceSeries.FromOhlcv(bars, bars, bars, bars, bars); + + // Assert + close.HasOpen.ShouldBeFalse(); + close.HasHighLow.ShouldBeFalse(); + close.HasVolume.ShouldBeFalse(); + + hlc.HasHighLow.ShouldBeTrue(); + hlc.HasOpen.ShouldBeFalse(); + hlc.HasVolume.ShouldBeFalse(); + + ohlc.HasOpen.ShouldBeTrue(); + ohlc.HasHighLow.ShouldBeTrue(); + ohlc.HasVolume.ShouldBeFalse(); + + ohlcv.HasOpen.ShouldBeTrue(); + ohlcv.HasHighLow.ShouldBeTrue(); + ohlcv.HasVolume.ShouldBeTrue(); + } + + [Fact] + public void AbsentComponentsThrowRatherThanFabricatingData() + { + // Arrange + // The abandoned branch's FromClose set Open = High = Low = Close, which makes ATR compute + // the absolute close-to-close change and call it a true range. Absence must be loud. + double[] bars = Ramp(10); + PriceSeries closeOnly = PriceSeries.FromClose(bars); + PriceSeries hlc = PriceSeries.FromHlc(bars, bars, bars); + PriceSeries ohlc = PriceSeries.FromOhlc(bars, bars, bars, bars); + + // Act + // (each accessor is invoked inside the assertions below) + + // Assert + Should.Throw(() => + { + _ = HighLength(closeOnly); + }); + + Should.Throw(() => + { + _ = LowLength(closeOnly); + }); + + Should.Throw(() => + { + _ = OpenLength(closeOnly); + }); + + Should.Throw(() => + { + _ = VolumeLength(closeOnly); + }); + + Should.Throw(() => + { + _ = OpenLength(hlc); + }); + + Should.Throw(() => + { + _ = VolumeLength(ohlc); + }); + } + + [Fact] + public void AsOfTruncatesEveryComponentAndKeepsBarIndicesAbsolute() + { + // Arrange + // Closes 1..100, so bar 50 holds 51.0 and still holds 51.0 after narrowing -- AsOf never + // rebases bar indices, which is exactly why Slice does not exist. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + PriceSeries narrowed = prices.AsOf(50); + ArgumentOutOfRangeException past = Should.Throw(() => + { + _ = prices.AsOf(100); + }); + + ArgumentOutOfRangeException negative = Should.Throw(() => + { + _ = prices.AsOf(-1); + }); + + // Assert + narrowed.BarCount.ShouldBe(51); + narrowed.Close.Length.ShouldBe(51); + narrowed.Close[50].ShouldBe(51.0); + past.ParamName.ShouldBe("bar"); + negative.ParamName.ShouldBe("bar"); + } + + [Fact] + public void AlignBarAlignsAnySingleOutputResult() + { + // Arrange + // CCI(20) with high = close + 1 and low = close - 1 has typical price + // ((c + 1) + (c - 1) + c) / 3 = c, i.e. the ramp itself. At bar 99 the mean of closes + // 81..100 is 90.5 and the mean absolute deviation of twenty consecutive integers about + // their mean is 5.0, so CCI = (100 - 90.5) / (0.015 * 5.0) = 9.5 / 0.075 = 126.6666... + double[] closes = Ramp(100); + double[] high = new double[100]; + double[] low = new double[100]; + for (int i = 0; i < 100; i++) + { + high[i] = closes[i] + 1.0; + low[i] = closes[i] - 1.0; + } + + PriceSeries prices = PriceSeries.FromHlc(high, low, closes); + + // Act + IndicatorSeries cci = prices.Align(TAMath.Cci(0, 99, high, low, closes, 20)); + + // Assert + cci.FirstBar.ShouldBe(19); + cci.WarmCount.ShouldBe(81); + cci.BarCount.ShouldBe(100); + cci.Latest.HasValue.ShouldBeTrue(); + cci.Latest.Value.ShouldBe(126.66666666666667, 1e-9); + } + + [Fact] + public void AlignWithASelectorBarAlignsOneComponentOfAMultiOutputResult() + { + // Arrange + // The selector, rather than a two-argument Align(metadata, array), is what prevents pairing + // one result's metadata with another result's array. + double[] closes = Ramp(100); + PriceSeries prices = PriceSeries.FromClose(closes); + BollingerBandsResult raw = TAMath.BollingerBands(0, 99, closes, 20, 2.0, 2.0, MAType.Sma); + + // Act + IndicatorSeries viaAlign = prices.Align(raw, static r => r.RealUpperBand); + IndicatorSeries viaFacade = prices.BollingerBands(20, 2.0, 2.0).Upper; + + // Assert + viaAlign.FirstBar.ShouldBe(19); + viaFacade.FirstBar.ShouldBe(19); + viaAlign.WarmCount.ShouldBe(viaFacade.WarmCount); + viaAlign.Latest.ShouldBe(viaFacade.Latest); + viaAlign.Latest.ShouldBe(90.5 + (2.0 * Math.Sqrt(33.25))); + } + + [Fact] + public void AlignRejectsNullArguments() + { + // Arrange + double[] closes = Ramp(100); + PriceSeries prices = PriceSeries.FromClose(closes); + BollingerBandsResult raw = TAMath.BollingerBands(0, 99, closes, 20, 2.0, 2.0, MAType.Sma); + + // Act + // (both calls are made inside the assertions below) + + // Assert + Should.Throw(() => + { + _ = prices.Align(null!); + }); + + Should.Throw(() => + { + _ = prices.Align(raw, null!); + }); + } + + [Fact] + public void AlignAcceptsTheDxFamilyNowThatItsSeedingLoopsRunTheRightNumberOfTimes() + { + // Arrange + // Adx, Dx, PlusDI and MinusDI used to share a loop-translation defect: their seeding loops + // ran period - 2 times where the reference runs period - 1, so BegIdx + NBElement landed + // exactly two past the end of the price series and every value was misaligned by two bars. + // Over 100 bars ADX(14) reported BegIdx = 27 and NBElement = 75, and 27 + 75 = 102 > 100, + // which made this the one family Align could not accept. The lookback of ADX(14) is + // 2 * 14 + 0 - 1 = 27, so the correct metadata is BegIdx = 27, NBElement = 73, summing to + // exactly the bar count. + double[] closes = Ramp(100); + double[] high = new double[100]; + double[] low = new double[100]; + for (int i = 0; i < 100; i++) + { + high[i] = closes[i] + 1.0; + low[i] = closes[i] - 1.0; + } + + PriceSeries prices = PriceSeries.FromHlc(high, low, closes); + AdxResult raw = TAMath.Adx(0, 99, high, low, closes, 14); + + // Act + IndicatorSeries adx = prices.Align(raw); + + // Assert + raw.RetCode.ShouldBe(RetCode.Success); + raw.BegIdx.ShouldBe(27); + raw.NBElement.ShouldBe(73); + (raw.BegIdx + raw.NBElement).ShouldBe(prices.BarCount); + adx.FirstBar.ShouldBe(27); + adx.LastBar.ShouldBe(99); + adx.WarmCount.ShouldBe(73); + } + + [Fact] + public void AlignBlamesItsOwnParameterWhenAnIndicatorReportsInconsistentMetadata() + { + // Arrange + // IndicatorSeries.Create throws naming nbElement or values, which name nothing the caller + // of Align passed -- and worse, they read as "your argument is bad" when the fault is the + // indicator's metadata. Align re-blames the one argument the caller can see. The stand-in + // for a defective indicator is a hand-built result claiming bars 90..109 of a 100-bar + // series. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + SmaResult inconsistent = new(RetCode.Success, 90, 20, new double[100]); + + // Act + ArgumentException thrown = Should.Throw(() => + { + _ = prices.Align(inconsistent); + }); + + // Assert + thrown.ParamName.ShouldBe("result"); + thrown.InnerException.ShouldBeOfType(); + thrown.Message.ShouldContain("90..109"); + } + + [Fact] + public void FactoriesRejectNonFinitePricesNamingTheComponentAndTheFirstBadBar() + { + // Arrange + // TA_INT_SMA keeps a running sum: once periodTotal is NaN, `NaN - finite` is NaN for ever + // after, so one bad tick at bar 40 poisons every SMA value from bar 40 to the end while + // RetCode stays Success and the indexer keeps returning a non-null double.NaN. Refusing at + // the boundary is the only place the failure is still diagnosable. + double[] withNaN = Ramp(100); + withNaN[40] = double.NaN; + double[] withInfinity = Ramp(100); + withInfinity[7] = double.PositiveInfinity; + double[] clean = Ramp(100); + + // Act + ArgumentException nan = Should.Throw(() => + { + _ = PriceSeries.FromClose(withNaN); + }); + + ArgumentException infinite = Should.Throw(() => + { + _ = PriceSeries.FromClose(withInfinity); + }); + + ArgumentException badHigh = Should.Throw(() => + { + _ = PriceSeries.FromHlc(withNaN, clean, clean); + }); + + ArgumentException badVolume = Should.Throw(() => + { + _ = PriceSeries.FromOhlcv(clean, clean, clean, clean, withNaN); + }); + + // Assert + nan.ParamName.ShouldBe("close"); + nan.Message.ShouldContain("bar 40"); + infinite.ParamName.ShouldBe("close"); + infinite.Message.ShouldContain("bar 7"); + badHigh.ParamName.ShouldBe("high"); + badVolume.ParamName.ShouldBe("volume"); + } + + [Fact] + public void AlignCopiesSoLaterMutationOfTheRawResultCannotReachTheSeries() + { + // Arrange + // result.Real is a public get-only property returning the live array, so a caller who + // post-processes it in place -- clipping, smoothing, back-filling -- would otherwise mutate + // an IndicatorSeries already handed out, possibly one another thread is reading. The type + // promises unconditional immutability, so Align pays for a copy. + double[] closes = Ramp(100); + PriceSeries prices = PriceSeries.FromClose(closes); + SmaResult raw = TAMath.Sma(0, 99, closes, 30); + + // Act + IndicatorSeries sma = prices.Align(raw); + raw.Real[70] = 999.0; + + // Assert + sma[99].ShouldBe(85.5); + sma.Latest.ShouldBe(85.5); + sma.WarmValues[70].ShouldBe(85.5); + } + + [Fact] + public void NoPublicAlignOverloadAcceptsABarCount() + { + // Arrange + // Align supplies BarCount itself, so a mismatched bar count is not merely discouraged -- + // it is unrepresentable. + MethodInfo[] alignOverloads = [.. typeof(PriceSeries) + .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) + .Where(m => m.Name == "Align")]; + + // Act + string[] parameterNames = [.. alignOverloads.SelectMany(m => m.GetParameters()).Select(p => p.Name!)]; + + // Assert + alignOverloads.Length.ShouldBeGreaterThan(0); + parameterNames.ShouldNotContain("barCount"); + parameterNames.ShouldNotContain("barcount"); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/VolatilityIndicatorsTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/VolatilityIndicatorsTests.cs new file mode 100644 index 00000000..c9d0f81e --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/VolatilityIndicatorsTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// Value-asserting tests for the volatility studies, and the refusal that matters most here: close-only +/// data can never produce a true range, so it must raise rather than quietly measure something else. +/// +public class VolatilityIndicatorsTests +{ + private static double[] Constant(int count, double value) + { + double[] values = new double[count]; + Array.Fill(values, value); + return values; + } + + private static double[] Ramp(int count) + { + double[] values = new double[count]; + for (int i = 0; i < count; i++) + { + values[i] = i + 1; + } + + return values; + } + + [Fact] + public void AtrOverAConstantTrueRangeEqualsThatTrueRangeExactly() + { + // Arrange + // Sixty bars of high 102, low 98, close 100. On every bar the true range is + // max(102 - 98, |102 - 100|, |98 - 100|) = 4.0, so the seed is the mean of fourteen 4s = 4 + // and each later bar is (4 * 13 + 4) / 14 = 4. Wilder's average of a constant is that + // constant, exactly -- no tolerance is warranted and none is used. + // This also pins the ATR normalisation fix on this branch: the running average used to be + // left multiplied by (period - 1) every bar and diverged geometrically to +Infinity. + PriceSeries prices = PriceSeries.FromHlc(Constant(60, 102.0), Constant(60, 98.0), Constant(60, 100.0)); + + // Act + IndicatorSeries atr = prices.Atr(14); + + // Assert + atr.Latest.ShouldBe(4.0); + atr[14].ShouldBe(4.0); + atr.FirstBar.ShouldBe(14); + atr.WarmCount.ShouldBe(46); + atr.BarCount.ShouldBe(60); + atr[13].ShouldBeNull(); + + foreach ((int bar, double value) in atr) + { + double.IsFinite(value).ShouldBeTrue($"ATR was {value} at bar {bar}."); + value.ShouldBe(4.0); + } + } + + [Fact] + public void AtrRefusesCloseOnlyDataRatherThanFabricatingARange() + { + // Arrange + // The abandoned branch's FromClose set Open = High = Low = Close, which makes ATR compute + // the absolute close-to-close change and present it as a true range. Refusing is the only + // honest answer: there is no high and no low to measure. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + // (the call is made inside the assertion below) + + // Assert + Should.Throw(() => + { + _ = prices.Atr(14); + }); + } + + [Fact] + public void StochRefusesCloseOnlyDataForTheSameReason() + { + // Arrange + // %K is the close's position inside the high/low range of the window. Without a range there + // is no position, and substituting the close for both bounds would report a constant zero. + PriceSeries prices = PriceSeries.FromClose(Ramp(100)); + + // Act + // (the call is made inside the assertion below) + + // Assert + Should.Throw(() => + { + _ = prices.Stoch(); + }); + } + + [Fact] + public void AtrReadsTheHighAndTheLowInThatOrderAndNotTheOtherWayRound() + { + // Arrange + // With high 102, low 98 and close 100 the true range is max(4, 2, 2) = 4 and ATR is 4. + // Reverse the two arguments and the range term becomes 98 - 102 = -4, so the true range + // collapses to max(-4, 2, 2) = 2 and ATR reads 2. Nothing else in the suite would notice + // the swap, because every other assertion about this fixture is shape rather than value. + PriceSeries correct = PriceSeries.FromHlc(Constant(60, 102.0), Constant(60, 98.0), Constant(60, 100.0)); + PriceSeries swapped = PriceSeries.FromHlc(Constant(60, 98.0), Constant(60, 102.0), Constant(60, 100.0)); + + // Act + IndicatorSeries fromCorrect = correct.Atr(14); + IndicatorSeries fromSwapped = swapped.Atr(14); + + // Assert + fromCorrect.Latest.ShouldBe(4.0); + fromSwapped.Latest.ShouldBe(2.0); + fromSwapped.Latest.ShouldNotBe(fromCorrect.Latest); + } + + [Fact] + public void AtrRisesAndFallsWithTheRangeSoEveryWarmBarHasItsOwnAnswer() + { + // Arrange + // Close pinned at 100 with a range that widens from 2 to 20 points over the series: + // high = 100 + r, low = 100 - r with r = 1 + bar / 10, so the true range on bar b is + // max(2r, r, r) = 2r = 2 + bar / 5 and grows monotonically. Wilder's average of a strictly + // increasing sequence is itself strictly increasing, so no two warm bars agree and it lags + // the current true range -- which a constant-range fixture cannot show at all. + double[] high = new double[100]; + double[] low = new double[100]; + double[] close = Constant(100, 100.0); + for (int bar = 0; bar < 100; bar++) + { + double halfRange = 1.0 + (bar / 10.0); + high[bar] = 100.0 + halfRange; + low[bar] = 100.0 - halfRange; + } + + PriceSeries prices = PriceSeries.FromHlc(high, low, close); + + // Act + IndicatorSeries atr = prices.Atr(14); + + // Assert + atr.FirstBar.ShouldBe(14); + atr.WarmCount.ShouldBe(86); + + double? previous = null; + int warmBars = 0; + foreach ((int bar, double value) in atr) + { + double trueRange = 2.0 + (bar / 5.0); + double.IsFinite(value).ShouldBeTrue($"ATR was {value} at bar {bar}."); + + // Wilder's average trails the current true range on a widening series, and never by + // more than the whole range: a value at or above the current true range would mean the + // smoothing is reading a later bar than it should. + value.ShouldBeLessThan(trueRange); + value.ShouldBeGreaterThan(trueRange / 2.0); + + if (previous is { } earlier) + { + value.ShouldBeGreaterThan(earlier); + } + + previous = value; + warmBars++; + } + + warmBars.ShouldBe(86); + } +} diff --git a/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/VolumeIndicatorsTests.cs b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/VolumeIndicatorsTests.cs new file mode 100644 index 00000000..1153beb0 --- /dev/null +++ b/tests/TechnicalAnalysis.Functions.UnitTests/HighLevel/VolumeIndicatorsTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Functions.UnitTests.HighLevel; + +/// +/// The one indicator on this surface with no warm-up at all: on-balance volume is warm from bar 0, +/// which makes it the case that proves FirstBar is read from the result rather than assumed +/// to be positive. It is also the only consumer of the volume component, so it is what keeps the +/// volume guard and the volume accessor honest. +/// +public class VolumeIndicatorsTests +{ + private static double[] Constant(int count, double value) + { + double[] values = new double[count]; + Array.Fill(values, value); + return values; + } + + [Fact] + public void ObvSignsEachBarsVolumeByTheDirectionOfTheCloseAndIsWarmFromBarZero() + { + // Arrange + // Closes 10, 11, 11, 9, 12 with volumes 100, 200, 300, 400, 500. The running total starts + // at the first bar's volume and then adds on an up close, subtracts on a down close and + // holds on an unchanged close: + // bar 0: 100 (seed) + // bar 1: 100 + 200 = 300 (11 > 10) + // bar 2: 300 (11 == 11) + // bar 3: 300 - 400 = -100 (9 < 11) + // bar 4: -100 + 500 = 400 (12 > 9) + double[] close = [10.0, 11.0, 11.0, 9.0, 12.0]; + double[] high = [11.0, 12.0, 12.0, 10.0, 13.0]; + double[] low = [9.0, 10.0, 10.0, 8.0, 11.0]; + double[] volume = [100.0, 200.0, 300.0, 400.0, 500.0]; + PriceSeries prices = PriceSeries.FromOhlcv(close, high, low, close, volume); + + // Act + IndicatorSeries obv = prices.Obv(); + + // Assert + obv.RetCode.ShouldBe(RetCode.Success); + obv.FirstBar.ShouldBe(0); + obv.LastBar.ShouldBe(4); + obv.WarmCount.ShouldBe(5); + obv.BarCount.ShouldBe(5); + obv[0].ShouldBe(100.0); + obv[1].ShouldBe(300.0); + obv[2].ShouldBe(300.0); + obv[3].ShouldBe(-100.0); + obv[4].ShouldBe(400.0); + obv.Latest.ShouldBe(400.0); + + // Warm from bar 0 means the bar-aligned projection has no NaN padding at all. + foreach (double value in obv.ToBarAlignedArray()) + { + double.IsNaN(value).ShouldBeFalse(); + } + } + + [Fact] + public void ObvRefusesAPriceSeriesThatCarriesNoVolume() + { + // Arrange + // Substituting a constant volume would silently turn on-balance volume into a signed bar + // counter, which is exactly the sort of plausible-looking wrong answer this API refuses to + // produce. + PriceSeries closeOnly = PriceSeries.FromClose(Constant(50, 100.0)); + PriceSeries hlc = PriceSeries.FromHlc(Constant(50, 101.0), Constant(50, 99.0), Constant(50, 100.0)); + + // Act + InvalidOperationException fromCloseOnly = Should.Throw(() => + { + _ = closeOnly.Obv(); + }); + + InvalidOperationException fromHlc = Should.Throw(() => + { + _ = hlc.Obv(); + }); + + // Assert + fromCloseOnly.Message.ShouldContain("FromOhlcv"); + fromHlc.Message.ShouldContain("FromOhlcv"); + } + + [Fact] + public void ObvOverAnEmptyFeedIsEmptyRatherThanThrowing() + { + // Arrange + PriceSeries empty = PriceSeries.FromOhlcv([], [], [], [], []); + + // Act + IndicatorSeries obv = empty.Obv(); + + // Assert + obv.RetCode.ShouldBe(RetCode.Success); + obv.HasValues.ShouldBeFalse(); + obv.BarCount.ShouldBe(0); + obv.FirstBar.ShouldBeNull(); + obv.Latest.ShouldBeNull(); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/BacktestSampleRunnerTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/BacktestSampleRunnerTests.cs new file mode 100644 index 00000000..d4775c9b --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/BacktestSampleRunnerTests.cs @@ -0,0 +1,172 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests; + +/// +/// End-to-end checks on the workload the executable actually runs. +/// +public class BacktestSampleRunnerTests +{ + [Fact] + public void TheSampleReportIsReproducibleForAGivenSeed() + { + // Arrange + CommandLineOptions options = CommandLineOptions.Parse(["--bars", "600", "--seed", "12345"]); + + // Act + string first = BacktestSampleRunner.Run(options); + string second = BacktestSampleRunner.Run(options); + + // Assert - the whole point of the deterministic generator. + first.ShouldBe(second); + } + + [Fact] + public void ChangingTheSeedChangesTheReport() + { + // Arrange / Act + string first = BacktestSampleRunner.Run(CommandLineOptions.Parse(["--bars", "600", "--seed", "1"])); + string second = BacktestSampleRunner.Run(CommandLineOptions.Parse(["--bars", "600", "--seed", "2"])); + + // Assert + first.ShouldNotBe(second); + } + + [Fact] + public void TheReportCoversEveryStrategyPlusTheBaseline() + { + // Arrange + CommandLineOptions options = CommandLineOptions.Parse(["--bars", "600"]); + + // Act + string report = BacktestSampleRunner.Run(options); + + // Assert + BacktestSampleRunner.CreateStrategies().Count.ShouldBe(5); + report.ShouldContain("RUN CONFIGURATION"); + report.ShouldContain("SIDE-BY-SIDE COMPARISON"); + report.ShouldContain("Buy and hold"); + report.ShouldNotContain("NaN"); + } + + [Fact] + public void RaisingTheFrictionsLowersEveryStrategyThatTrades() + { + // Arrange + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(800, seed: 606); + BacktestOptions cheap = new() { InitialCapital = 100_000, CommissionBps = 0, SlippageBps = 0 }; + BacktestOptions expensive = cheap with { CommissionBps = 50, SlippageBps = 25 }; + + // Act + IReadOnlyList cheapRuns = BacktestSampleRunner.RunAll(BacktestSampleRunner.CreateStrategies(), bars, cheap); + IReadOnlyList expensiveRuns = BacktestSampleRunner.RunAll(BacktestSampleRunner.CreateStrategies(), bars, expensive); + + // Assert + for (int i = 0; i < cheapRuns.Count; i++) + { + if (cheapRuns[i].Trades.Count == 0) + { + continue; + } + + expensiveRuns[i].FinalEquity.ShouldBeLessThan( + cheapRuns[i].FinalEquity, + $"{cheapRuns[i].StrategyName} should be worse once it pays 75 bp a fill"); + } + } + + [Fact] + public void ARunOverACsvFileUsesThatDataInsteadOfTheGenerator() + { + // Arrange + string path = Path.Combine(Path.GetTempPath(), $"talib-backtest-{Guid.NewGuid():N}.csv"); + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(120, seed: 808); + WriteCsv(path, bars); + + try + { + CommandLineOptions options = CommandLineOptions.Parse(["--csv", path]); + + // Act + IReadOnlyList loaded = BacktestSampleRunner.LoadBars(options, out string description); + string report = BacktestSampleRunner.Run(options); + + // Assert + loaded.Count.ShouldBe(bars.Count); + loaded[0].Close.ShouldBe(bars[0].Close, 1e-6); + description.ShouldContain(path); + report.ShouldContain(path); + report.ShouldContain("SIDE-BY-SIDE COMPARISON"); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void AnEmptyCsvFileProducesAnExplanationRatherThanACrash() + { + // Arrange + string path = Path.Combine(Path.GetTempPath(), $"talib-backtest-{Guid.NewGuid():N}.csv"); + File.WriteAllText(path, "Date,Open,High,Low,Close,Volume\n"); + + try + { + CommandLineOptions options = CommandLineOptions.Parse(["--csv", path]); + + // Act + string report = BacktestSampleRunner.Run(options); + + // Assert + report.ShouldContain("nothing to backtest"); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public void EveryStrategyRunsOverTheDefaultSeriesWithoutLookingAhead() + { + // Arrange + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(1_500); + BacktestOptions options = new() { InitialCapital = 100_000, AllowShort = true }; + + // Act + IReadOnlyList results = BacktestSampleRunner.RunAll(BacktestSampleRunner.CreateStrategies(), bars, options); + + // Assert - no LookAheadException anywhere, and every run is internally consistent. + results.Count.ShouldBe(5); + foreach (BacktestResult result in results) + { + result.EquityCurve.Count.ShouldBe(bars.Count); + result.FinalEquity.ShouldBe(result.EquityCurve[^1].Equity); + result.Metrics.TradeCount.ShouldBe(result.Trades.Count); + double.IsFinite(result.Metrics.Sharpe).ShouldBeTrue(); + } + } + + private static void WriteCsv(string path, IReadOnlyList bars) + { + using StreamWriter writer = new(path); + writer.WriteLine("Date,Open,High,Low,Close,Volume"); + foreach (Bar bar in bars) + { + writer.WriteLine(string.Format( + CultureInfo.InvariantCulture, + "{0:yyyy-MM-dd},{1:R},{2:R},{3:R},{4:R},{5:R}", + bar.Timestamp, + bar.Open, + bar.High, + bar.Low, + bar.Close, + bar.Volume)); + } + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/CommandLineOptionsTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/CommandLineOptionsTests.cs new file mode 100644 index 00000000..c4cf342a --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/CommandLineOptionsTests.cs @@ -0,0 +1,153 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests; + +public class CommandLineOptionsTests +{ + [Fact] + public void AnEmptyCommandLineYieldsTheDocumentedDefaults() + { + // Arrange / Act + CommandLineOptions options = CommandLineOptions.Parse([]); + + // Assert + options.ShowHelp.ShouldBeFalse(); + options.CsvPath.ShouldBeNull(); + options.BarCount.ShouldBe(1_500); + options.Seed.ShouldBe(SyntheticSeriesGenerator.DefaultSeed); + options.ShowTradeLog.ShouldBeFalse(); + options.Backtest.InitialCapital.ShouldBe(100_000.0); + options.Backtest.CommissionBps.ShouldBe(5.0); + options.Backtest.SlippageBps.ShouldBe(2.0); + options.Backtest.BarsPerYear.ShouldBe(252); + options.Backtest.AllowShort.ShouldBeFalse(); + } + + [Fact] + public void EveryOptionIsParsedWithTheInvariantCulture() + { + // Arrange + string[] args = + [ + "--csv", "/tmp/prices.csv", + "--bars", "3000", + "--seed", "-17", + "--capital", "250000.5", + "--commission-bps", "12.5", + "--slippage-bps", "0.75", + "--bars-per-year", "365", + "--allow-short", + "--trade-log" + ]; + + // Act + CommandLineOptions options = CommandLineOptions.Parse(args); + + // Assert + options.CsvPath.ShouldBe("/tmp/prices.csv"); + options.BarCount.ShouldBe(3_000); + options.Seed.ShouldBe(-17); + options.ShowTradeLog.ShouldBeTrue(); + options.Backtest.InitialCapital.ShouldBe(250_000.5); + options.Backtest.CommissionBps.ShouldBe(12.5); + options.Backtest.SlippageBps.ShouldBe(0.75); + options.Backtest.BarsPerYear.ShouldBe(365); + options.Backtest.AllowShort.ShouldBeTrue(); + } + + [Theory] + [InlineData("-h")] + [InlineData("--help")] + public void HelpShortCircuitsTheRestOfTheCommandLine(string flag) + { + // Arrange / Act - the trailing garbage must not be reported, because help wins. + CommandLineOptions options = CommandLineOptions.Parse([flag, "--nonsense"]); + + // Assert + options.ShowHelp.ShouldBeTrue(); + } + + [Fact] + public void TheUsageTextDocumentsEveryOption() + { + // Arrange / Act + string usage = CommandLineOptions.Usage; + + // Assert + usage.ShouldContain("--csv"); + usage.ShouldContain("--capital"); + usage.ShouldContain("--commission-bps"); + usage.ShouldContain("--slippage-bps"); + usage.ShouldContain("--seed"); + usage.ShouldContain("--bars"); + usage.ShouldContain("--allow-short"); + usage.ShouldContain("--help"); + } + + [Fact] + public void AnUnknownOptionIsRejected() + { + // Arrange / Act + FormatException exception = Should.Throw(() => CommandLineOptions.Parse(["--turbo"])); + + // Assert + exception.Message.ShouldContain("--turbo"); + } + + [Fact] + public void AnOptionMissingItsValueIsRejected() + { + // Arrange / Act + FormatException exception = Should.Throw(() => CommandLineOptions.Parse(["--capital"])); + + // Assert + exception.Message.ShouldContain("requires a value"); + } + + [Theory] + [InlineData("--bars", "not-a-number")] + [InlineData("--capital", "lots")] + [InlineData("--bars-per-year", "3.5")] + public void AnUnparsableValueIsRejected(string option, string value) + { + // Arrange / Act / Assert + Should.Throw(() => CommandLineOptions.Parse([option, value])); + } + + [Theory] + [InlineData("--bars", "0")] + [InlineData("--bars-per-year", "0")] + public void AValueBelowItsMinimumIsRejected(string option, string value) + { + // Arrange / Act + FormatException exception = Should.Throw(() => CommandLineOptions.Parse([option, value])); + + // Assert + exception.Message.ShouldContain("at least"); + } + + [Theory] + [InlineData("--capital", "0")] + [InlineData("--capital", "-1000")] + [InlineData("--commission-bps", "-1")] + [InlineData("--slippage-bps", "-1")] + public void AnOutOfRangeBacktestSettingIsRejectedByValidation(string option, string value) + { + // Arrange / Act + FormatException exception = Should.Throw(() => CommandLineOptions.Parse([option, value])); + + // Assert + exception.Message.ShouldContain("BacktestOptions"); + } + + [Fact] + public void ParseRejectsANullCommandLine() + { + // Arrange / Act / Assert + Should.Throw(() => CommandLineOptions.Parse(null!)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/CsvBarLoaderTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/CsvBarLoaderTests.cs new file mode 100644 index 00000000..636ae4f7 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/CsvBarLoaderTests.cs @@ -0,0 +1,296 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Data; + +public class CsvBarLoaderTests +{ + private static IReadOnlyList Load(string csv) + { + using StringReader reader = new(csv); + return CsvBarLoader.Load(reader); + } + + [Fact] + public void TheDocumentedHeaderLayoutLoadsExactly() + { + // Arrange + const string Csv = """ + Date,Open,High,Low,Close,Volume + 2024-01-02,100.5,102.25,99.75,101.0,1500000 + 2024-01-03,101.0,103.5,100.5,103.25,1750000 + """; + + // Act + IReadOnlyList bars = Load(Csv); + + // Assert + bars.Count.ShouldBe(2); + bars[0].Timestamp.ShouldBe(new DateTime(2024, 1, 2, 0, 0, 0, DateTimeKind.Utc)); + bars[0].Open.ShouldBe(100.5); + bars[0].High.ShouldBe(102.25); + bars[0].Low.ShouldBe(99.75); + bars[0].Close.ShouldBe(101.0); + bars[0].Volume.ShouldBe(1_500_000.0); + bars[1].Close.ShouldBe(103.25); + bars.ShouldAllBe(bar => bar.IsWellFormed()); + } + + [Fact] + public void ColumnOrderDoesNotMatterAndExtraColumnsAreIgnored() + { + // Arrange - a typical broker export: shuffled columns plus an adjusted close. + const string Csv = """ + Close,Adj Close,High,Timestamp,Low,Volume,Open + 101.0,100.9,102.0,2024-01-02,99.0,1000,100.0 + """; + + // Act + IReadOnlyList bars = Load(Csv); + + // Assert + bars.Count.ShouldBe(1); + bars[0].Open.ShouldBe(100.0); + bars[0].High.ShouldBe(102.0); + bars[0].Low.ShouldBe(99.0); + bars[0].Close.ShouldBe(101.0); + bars[0].Volume.ShouldBe(1_000.0); + } + + [Fact] + public void BlankLinesAndQuotedFieldsAreTolerated() + { + // Arrange + const string Csv = """ + "Date","Open","High","Low","Close","Volume" + + "2024-01-02","100.0","101.0","99.0","100.5","1000" + + "2024-01-03","100.5","101.5","100.0","101.0","1100" + """; + + // Act + IReadOnlyList bars = Load(Csv); + + // Assert + bars.Count.ShouldBe(2); + bars[1].Close.ShouldBe(101.0); + } + + [Fact] + public void AMissingVolumeColumnDefaultsToZero() + { + // Arrange + const string Csv = """ + Date,Open,High,Low,Close + 2024-01-02,100.0,101.0,99.0,100.5 + """; + + // Act + IReadOnlyList bars = Load(Csv); + + // Assert + bars[0].Volume.ShouldBe(0.0); + bars[0].IsWellFormed().ShouldBeTrue(); + } + + [Fact] + public void AMissingPriceColumnIsRejected() + { + // Arrange + const string Csv = """ + Date,Open,High,Low,Volume + 2024-01-02,100.0,101.0,99.0,1000 + """; + + // Act + FormatException exception = Should.Throw(() => Load(Csv)); + + // Assert + exception.Message.ShouldContain("Close"); + } + + [Fact] + public void AMissingDateColumnIsRejected() + { + // Arrange + const string Csv = """ + Open,High,Low,Close + 100.0,101.0,99.0,100.5 + """; + + // Act + FormatException exception = Should.Throw(() => Load(Csv)); + + // Assert + exception.Message.ShouldContain("date column"); + } + + [Fact] + public void AnUnparsableNumberIsReportedWithItsLineNumber() + { + // Arrange - line 3 carries a price that is not a number at all. + const string Csv = """ + Date,Open,High,Low,Close,Volume + 2024-01-02,100.0,101.0,99.0,100.5,1000 + 2024-01-03,100.0,101.0,n/a,100.5,1000 + """; + + // Act + FormatException exception = Should.Throw(() => Load(Csv)); + + // Assert + exception.Message.ShouldContain("Line 3"); + } + + [Fact] + public void AnUnparsableDateIsReportedWithItsLineNumber() + { + // Arrange + const string Csv = """ + Date,Open,High,Low,Close,Volume + not-a-date,100.0,101.0,99.0,100.5,1000 + """; + + // Act + FormatException exception = Should.Throw(() => Load(Csv)); + + // Assert + exception.Message.ShouldContain("Line 2"); + exception.Message.ShouldContain("date"); + } + + [Fact] + public void ATruncatedRowIsRejected() + { + // Arrange + const string Csv = """ + Date,Open,High,Low,Close,Volume + 2024-01-02,100.0,101.0 + """; + + // Act + FormatException exception = Should.Throw(() => Load(Csv)); + + // Assert + exception.Message.ShouldContain("Line 2"); + exception.Message.ShouldContain("fields"); + } + + [Fact] + public void AnEmptySourceIsRejected() + { + // Arrange / Act / Assert + Should.Throw(() => Load(string.Empty)); + } + + [Fact] + public void AHeaderWithNoRowsLoadsAnEmptySeries() + { + // Arrange + const string Csv = "Date,Open,High,Low,Close,Volume"; + + // Act + IReadOnlyList bars = Load(Csv); + + // Assert + bars.ShouldBeEmpty(); + } + + [Fact] + public void ValidateFlagsOutOfOrderTimestamps() + { + // Arrange + const string Csv = """ + Date,Open,High,Low,Close,Volume + 2024-01-03,100.0,101.0,99.0,100.5,1000 + 2024-01-02,100.0,101.0,99.0,100.5,1000 + """; + + // Act + IReadOnlyList problems = CsvBarLoader.Validate(Load(Csv)); + + // Assert + problems.Count.ShouldBe(1); + problems[0].ShouldContain("not strictly after"); + } + + [Fact] + public void ValidateFlagsAnInconsistentBar() + { + // Arrange - the high is below the close, which is impossible. + const string Csv = """ + Date,Open,High,Low,Close,Volume + 2024-01-02,100.0,100.5,99.0,120.0,1000 + """; + + // Act + IReadOnlyList problems = CsvBarLoader.Validate(Load(Csv)); + + // Assert + problems.Count.ShouldBe(1); + problems[0].ShouldContain("not well formed"); + } + + [Fact] + public void ValidateAcceptsACleanSeries() + { + // Arrange + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(200, seed: 99); + + // Act + IReadOnlyList problems = CsvBarLoader.Validate(bars); + + // Assert + problems.ShouldBeEmpty(); + } + + [Fact] + public void LoadFileReadsFromDiskAndReportsAMissingFile() + { + // Arrange + string path = Path.Combine(Path.GetTempPath(), $"talib-backtest-{Guid.NewGuid():N}.csv"); + File.WriteAllText(path, "Date,Open,High,Low,Close,Volume\n2024-01-02,100.0,101.0,99.0,100.5,1000\n"); + + try + { + // Act + IReadOnlyList bars = CsvBarLoader.LoadFile(path); + + // Assert + bars.Count.ShouldBe(1); + bars[0].Close.ShouldBe(100.5); + } + finally + { + File.Delete(path); + } + + Should.Throw(() => CsvBarLoader.LoadFile(path)); + Should.Throw(() => CsvBarLoader.LoadFile(" ")); + } + + [Fact] + public void ALoadedSeriesCanBeBacktestedEndToEnd() + { + // Arrange - the whole point of the loader: user data flows straight into the engine. + const string Csv = """ + Date,Open,High,Low,Close,Volume + 2024-01-02,100.0,101.0,99.0,100.0,1000 + 2024-01-03,100.0,111.0,99.0,110.0,1000 + 2024-01-04,110.0,121.0,109.0,120.0,1000 + """; + + BacktestOptions options = new() { InitialCapital = 10_000, CommissionBps = 0, SlippageBps = 0 }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(new BuyAndHoldStrategy(), Load(Csv)); + + // Assert - bought at 100 on 2024-01-03, liquidated at 120 on 2024-01-04. + result.Trades.Count.ShouldBe(1); + result.FinalEquity.ShouldBe(12_000.0, 1e-9); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/DeterministicRandomTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/DeterministicRandomTests.cs new file mode 100644 index 00000000..c14a4b0c --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/DeterministicRandomTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Data; + +public class DeterministicRandomTests +{ + [Fact] + public void TheSameSeedReplaysTheSameSequence() + { + // Arrange + DeterministicRandom first = new(2024); + DeterministicRandom second = new(2024); + + // Act + ulong[] a = [.. Enumerable.Range(0, 100).Select(_ => first.NextUInt64())]; + ulong[] b = [.. Enumerable.Range(0, 100).Select(_ => second.NextUInt64())]; + + // Assert + a.ShouldBe(b); + } + + [Fact] + public void DifferentSeedsDiverge() + { + // Arrange + DeterministicRandom first = new(1); + DeterministicRandom second = new(2); + + // Act + ulong[] a = [.. Enumerable.Range(0, 100).Select(_ => first.NextUInt64())]; + ulong[] b = [.. Enumerable.Range(0, 100).Select(_ => second.NextUInt64())]; + + // Assert + a.ShouldNotBe(b); + } + + [Fact] + public void UniformDeviatesStayInTheHalfOpenUnitInterval() + { + // Arrange + DeterministicRandom random = new(31); + + // Act + double[] samples = [.. Enumerable.Range(0, 200_000).Select(_ => random.NextDouble())]; + + // Assert + samples.ShouldAllBe(value => value >= 0.0 && value < 1.0); + samples.Average().ShouldBe(0.5, 0.01); + } + + [Fact] + public void GaussianDeviatesHaveTheExpectedShape() + { + // Arrange + DeterministicRandom random = new(97); + double[] samples = [.. Enumerable.Range(0, 200_000).Select(_ => random.NextGaussian())]; + + // Act + double mean = samples.Average(); + double variance = samples.Sum(value => (value - mean) * (value - mean)) / (samples.Length - 1); + + // Assert - 200k draws puts the standard error of the mean near 0.0022, so 0.02 is a safe band. + mean.ShouldBe(0.0, 0.02); + Math.Sqrt(variance).ShouldBe(1.0, 0.02); + samples.ShouldAllBe(value => double.IsFinite(value)); + + // Roughly two thirds of the mass must lie within one standard deviation. + (samples.Count(value => Math.Abs(value) <= 1.0) / (double)samples.Length).ShouldBe(0.6827, 0.01); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/SyntheticSeriesGeneratorTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/SyntheticSeriesGeneratorTests.cs new file mode 100644 index 00000000..db20086e --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Data/SyntheticSeriesGeneratorTests.cs @@ -0,0 +1,125 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Data; + +public class SyntheticSeriesGeneratorTests +{ + [Fact] + public void TheSameSeedAlwaysProducesTheSameSeries() + { + // Arrange / Act + IReadOnlyList first = SyntheticSeriesGenerator.Generate(250, seed: 4242); + IReadOnlyList second = SyntheticSeriesGenerator.Generate(250, seed: 4242); + + // Assert - bit-for-bit, which is what makes the sample's printed numbers reproducible. + first.Count.ShouldBe(second.Count); + for (int i = 0; i < first.Count; i++) + { + first[i].ShouldBe(second[i]); + } + } + + [Fact] + public void DifferentSeedsProduceDifferentSeries() + { + // Arrange / Act + IReadOnlyList first = SyntheticSeriesGenerator.Generate(250, seed: 1); + IReadOnlyList second = SyntheticSeriesGenerator.Generate(250, seed: 2); + + // Assert + first.Select(bar => bar.Close).ShouldNotBe(second.Select(bar => bar.Close)); + } + + [Fact] + public void EveryGeneratedBarIsWellFormed() + { + // Arrange / Act + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(2_000, seed: 7); + + // Assert + bars.ShouldAllBe(bar => bar.IsWellFormed()); + CsvBarLoader.Validate(bars).ShouldBeEmpty(); + } + + [Fact] + public void TimestampsAreStrictlyAscendingWeekdays() + { + // Arrange / Act + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(500, seed: 11); + + // Assert + bars.ShouldAllBe(bar => bar.Timestamp.DayOfWeek != DayOfWeek.Saturday && bar.Timestamp.DayOfWeek != DayOfWeek.Sunday); + for (int i = 1; i < bars.Count; i++) + { + bars[i].Timestamp.ShouldBeGreaterThan(bars[i - 1].Timestamp); + } + } + + [Fact] + public void TheSeriesDriftsUpwardsOverTheLongRun() + { + // Arrange - a positive drift is what makes the trend-following comparisons interesting. + // Averaged over ten seeds the drift must dominate the noise. + List totalReturns = []; + + // Act + for (int seed = 0; seed < 10; seed++) + { + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(2_520, seed: seed, annualDrift: 0.12); + totalReturns.Add((bars[^1].Close / bars[0].Open) - 1.0); + } + + // Assert - 10 years of 12% drift compounds to roughly +230%; require at least a doubling on average. + totalReturns.Average().ShouldBeGreaterThan(1.0); + } + + [Fact] + public void ZeroBarsProducesAnEmptySeries() + { + // Arrange / Act + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(0); + + // Assert + bars.ShouldBeEmpty(); + } + + [Fact] + public void TheFirstBarOpensAtTheRequestedStartPrice() + { + // Arrange / Act + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(10, seed: 3, startPrice: 42.5); + + // Assert + bars[0].Open.ShouldBe(42.5); + } + + [Fact] + public void TheRequestedStartDateIsHonoured() + { + // Arrange + DateTime start = new(2021, 3, 15, 0, 0, 0, DateTimeKind.Utc); + + // Act + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(5, seed: 3, startDate: start); + + // Assert + bars[0].Timestamp.ShouldBe(start); + bars.Count.ShouldBe(5); + } + + [Theory] + [InlineData(-1, 100.0, 0.2, 252)] + [InlineData(10, 0.0, 0.2, 252)] + [InlineData(10, 100.0, -0.1, 252)] + [InlineData(10, 100.0, 0.2, 0)] + public void AnInvalidConfigurationIsRejected(int barCount, double startPrice, double volatility, int barsPerYear) + { + // Arrange / Act / Assert + Should.Throw( + () => SyntheticSeriesGenerator.Generate(barCount, 1, startPrice, 0.1, volatility, barsPerYear)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BacktestOptionsTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BacktestOptionsTests.cs new file mode 100644 index 00000000..98b8e0fd --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BacktestOptionsTests.cs @@ -0,0 +1,101 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +public class BacktestOptionsTests +{ + [Fact] + public void BasisPointsAreConvertedToDecimalRates() + { + // Arrange + BacktestOptions options = new() { CommissionBps = 12.5, SlippageBps = 3.0 }; + + // Act / Assert - 1 bp = 0.01% = 0.0001. + options.CommissionRate.ShouldBe(0.00125, 1e-15); + options.SlippageRate.ShouldBe(0.0003, 1e-15); + } + + [Fact] + public void TheDefaultsAreValid() + { + // Arrange + BacktestOptions options = new(); + + // Act / Assert + Should.NotThrow(options.Validate); + options.InitialCapital.ShouldBe(100_000.0); + options.BarsPerYear.ShouldBe(252); + options.CloseOpenPositionAtEnd.ShouldBeTrue(); + options.Sizing.ShouldBe(PositionSizing.FixedFraction); + } + + [Theory] + [InlineData(0.0, 5.0, 2.0, 1.0, 252)] + [InlineData(-1.0, 5.0, 2.0, 1.0, 252)] + [InlineData(100.0, -1.0, 2.0, 1.0, 252)] + [InlineData(100.0, 5.0, -1.0, 1.0, 252)] + [InlineData(100.0, 5.0, 20_000.0, 1.0, 252)] + [InlineData(100.0, 5.0, 2.0, 0.0, 252)] + [InlineData(100.0, 5.0, 2.0, 1.5, 252)] + [InlineData(100.0, 5.0, 2.0, 1.0, 0)] + [InlineData(double.NaN, 5.0, 2.0, 1.0, 252)] + public void AnImpossibleConfigurationIsRejected( + double capital, + double commissionBps, + double slippageBps, + double fraction, + int barsPerYear) + { + // Arrange + BacktestOptions options = new() + { + InitialCapital = capital, + CommissionBps = commissionBps, + SlippageBps = slippageBps, + PositionFraction = fraction, + BarsPerYear = barsPerYear + }; + + // Act / Assert + Should.Throw(options.Validate); + } + + [Fact] + public void TheEngineValidatesItsOptionsOnConstruction() + { + // Arrange + BacktestOptions options = new() { InitialCapital = -1.0 }; + + // Act / Assert + Should.Throw(() => new BacktestEngine(options)); + Should.Throw(() => new BacktestEngine(null!)); + } + + [Fact] + public void TheEngineRejectsNullArguments() + { + // Arrange + BacktestEngine engine = new(); + + // Act / Assert + Should.Throw(() => engine.Run(null!, [])); + Should.Throw(() => engine.Run(new BuyAndHoldStrategy(), null!)); + } + + [Fact] + public void SlippageAtOrAboveOneHundredPercentIsRejectedBecauseASellWouldFillAtZero() + { + // Arrange + BacktestOptions options = new() { SlippageBps = 10_000.0 }; + + // Act + ArgumentException exception = Should.Throw(options.Validate); + + // Assert + exception.Message.ShouldContain("SlippageBps"); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BarWindowTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BarWindowTests.cs new file mode 100644 index 00000000..da53257d --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BarWindowTests.cs @@ -0,0 +1,131 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +public class BarWindowTests +{ + [Fact] + public void CountReportsOnlyVisibleBarsSoTheSeriesLengthNeverLeaks() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12, 13, 14, 15]); + + // Act + BarWindow window = new(bars, 2); + + // Assert + window.CurrentIndex.ShouldBe(2); + window.Count.ShouldBe(3); + } + + [Fact] + public void ReadingTheNextBarThrowsLookAhead() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12, 13]); + BarWindow window = new(bars, 1); + + // Act + LookAheadException exception = Should.Throw(() => window[2]); + + // Assert + exception.Message.ShouldContain("Look-ahead bias detected"); + exception.Message.ShouldContain("index 2"); + } + + [Fact] + public void ReadingTheLastBarOfTheSeriesFromAnEarlierPositionThrowsLookAhead() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12, 13]); + BarWindow window = new(bars, 0); + + // Act / Assert + Should.Throw(() => window[3]); + } + + [Fact] + public void ReadingPastAndCurrentBarsIsAllowed() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12, 13]); + BarWindow window = new(bars, 2); + + // Act + Bar first = window[0]; + Bar current = window[2]; + + // Assert + first.Close.ShouldBe(10); + current.Close.ShouldBe(12); + window.Current.Close.ShouldBe(12); + window.Ago(0).Close.ShouldBe(12); + window.Ago(2).Close.ShouldBe(10); + } + + [Fact] + public void NegativeIndexThrowsArgumentOutOfRangeNotLookAhead() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12]); + BarWindow window = new(bars, 1); + + // Act / Assert + Should.Throw(() => window[-1]); + } + + [Fact] + public void SteppingBackBeforeTheStartOfTheSeriesThrowsArgumentOutOfRange() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12]); + BarWindow window = new(bars, 1); + + // Act / Assert + Should.Throw(() => window.Ago(2)); + } + + [Fact] + public void AnUnpositionedWindowHasNoCurrentBar() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12]); + + // Act + BarWindow window = new(bars); + + // Assert + window.CurrentIndex.ShouldBe(-1); + window.Count.ShouldBe(0); + Should.Throw(() => window.Current); + } + + [Fact] + public void AnEmptySeriesProducesAnUnpositionedWindow() + { + // Arrange + IReadOnlyList bars = []; + + // Act + BarWindow window = new(bars); + + // Assert + window.Count.ShouldBe(0); + Should.Throw(() => window[0]); + } + + [Fact] + public void ConstructingBeyondTheEndOfTheSeriesThrows() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12]); + + // Act / Assert + Should.Throw(() => new BarWindow(bars, 3)); + Should.Throw(() => new BarWindow(bars, -2)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BuyAndHoldBaselineTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BuyAndHoldBaselineTests.cs new file mode 100644 index 00000000..f42f5a2a --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/BuyAndHoldBaselineTests.cs @@ -0,0 +1,142 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +/// +/// The baseline must be exactly the return of the underlying series from the second bar's open to the last +/// bar's close, net of one round trip of costs β€” nothing more, nothing less. If it were not, every +/// comparison in the report would be measured against the wrong yardstick. +/// +public class BuyAndHoldBaselineTests +{ + private const double Capital = 100_000.0; + + private static IReadOnlyList Series() + { + return SyntheticSeriesGenerator.Generate(400, seed: 12_345); + } + + [Fact] + public void WithoutCostsItEqualsTheUnderlyingSeriesReturn() + { + // Arrange + IReadOnlyList bars = Series(); + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = 0, + SlippageBps = 0 + }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(new BuyAndHoldStrategy(), bars); + + // Assert - bought at the open of bar 1, liquidated at the close of the last bar. + double underlyingReturn = (bars[^1].Close / bars[1].Open) - 1.0; + + result.Trades.Count.ShouldBe(1); + result.Trades[0].EntryIndex.ShouldBe(1); + result.Trades[0].ExitIndex.ShouldBe(bars.Count - 1); + result.Metrics.TotalReturn.ShouldBe(underlyingReturn, 1e-9); + result.FinalEquity.ShouldBe(Capital * (1.0 + underlyingReturn), 1e-6); + } + + [Fact] + public void WithCostsItEqualsTheUnderlyingReturnNetOfExactlyOneRoundTrip() + { + // Arrange + const double CommissionBps = 7.5; + const double SlippageBps = 3.0; + const double CommissionRate = CommissionBps / 10_000.0; + const double SlippageRate = SlippageBps / 10_000.0; + + IReadOnlyList bars = Series(); + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = CommissionBps, + SlippageBps = SlippageBps + }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(new BuyAndHoldStrategy(), bars); + + // Assert - closed form: + // final = C * (lastClose * (1 - s) * (1 - c)) / (firstOpen * (1 + s) * (1 + c)) + double entryPrice = bars[1].Open; + double exitPrice = bars[^1].Close; + double expectedFinal = + Capital + * (exitPrice * (1.0 - SlippageRate) * (1.0 - CommissionRate)) + / (entryPrice * (1.0 + SlippageRate) * (1.0 + CommissionRate)); + + result.Trades.Count.ShouldBe(1); + result.FinalEquity.ShouldBe(expectedFinal, 1e-6); + + // Exactly one round trip of costs: two fills, no more. + double grossReturn = (exitPrice / entryPrice) - 1.0; + double costDrag = 1.0 + grossReturn - (result.FinalEquity / Capital); + costDrag.ShouldBeGreaterThan(0.0); + result.Metrics.TotalReturn.ShouldBeLessThan(grossReturn); + } + + [Fact] + public void ItIsExposedOnEveryBarExceptTheFirst() + { + // Arrange + IReadOnlyList bars = Series(); + + // Act + BacktestResult result = new BacktestEngine(new BacktestOptions { InitialCapital = Capital }) + .Run(new BuyAndHoldStrategy(), bars); + + // Assert - flat on bar 0 (the signal has not been filled yet) and on the last bar (liquidated), + // long on every bar in between. + result.EquityCurve[0].IsInPosition.ShouldBeFalse(); + result.EquityCurve[^1].IsInPosition.ShouldBeFalse(); + result.EquityCurve.Skip(1).Take(bars.Count - 2).ShouldAllBe(point => point.SignedQuantity > 0.0); + result.Metrics.Exposure.ShouldBe((bars.Count - 2.0) / bars.Count, 1e-12); + } + + [Fact] + public void KeepingThePositionOpenAtTheEndLeavesItUnrealisedAndUncounted() + { + // Arrange + IReadOnlyList bars = Series(); + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = 0, + SlippageBps = 0, + CloseOpenPositionAtEnd = false + }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(new BuyAndHoldStrategy(), bars); + + // Assert - no completed round trip, but the equity still marks to market at the final close. + result.Trades.ShouldBeEmpty(); + result.EquityCurve[^1].IsInPosition.ShouldBeTrue(); + result.FinalEquity.ShouldBe(Capital * bars[^1].Close / bars[1].Open, 1e-6); + } + + [Fact] + public void ItEmitsExactlyOneEntrySignalForTheWholeRun() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([10, 11, 12, 13, 14, 15]); + BuyAndHoldStrategy strategy = new(); + StrategyDriver driver = new(strategy, bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert + signals[0].ShouldBe(Signal.EnterLong); + signals.Skip(1).ShouldAllBe(signal => signal == Signal.Hold); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/DegenerateInputTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/DegenerateInputTests.cs new file mode 100644 index 00000000..f0a80a20 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/DegenerateInputTests.cs @@ -0,0 +1,182 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +/// +/// Inputs that a real user will hit sooner or later: no data at all, less data than the longest lookback, a +/// single bar, or a market that never moves. None of them may throw, and none may produce a NaN. +/// +public class DegenerateInputTests +{ + public static TheoryData StrategyNames => [ + nameof(SmaCrossoverStrategy), + nameof(RsiMeanReversionStrategy), + nameof(MacdTrendStrategy), + nameof(BollingerBreakoutStrategy), + nameof(BuyAndHoldStrategy) + ]; + + private static IStrategy Create(string name) + { + return name switch + { + nameof(SmaCrossoverStrategy) => new SmaCrossoverStrategy(20, 50), + nameof(RsiMeanReversionStrategy) => new RsiMeanReversionStrategy(), + nameof(MacdTrendStrategy) => new MacdTrendStrategy(), + nameof(BollingerBreakoutStrategy) => new BollingerBreakoutStrategy(), + _ => new BuyAndHoldStrategy() + }; + } + + [Theory] + [MemberData(nameof(StrategyNames))] + public void AnEmptySeriesProducesAnEmptyRunRatherThanAnException(string strategyName) + { + // Arrange + BacktestEngine engine = new(new BacktestOptions { InitialCapital = 50_000 }); + + // Act + BacktestResult result = engine.Run(Create(strategyName), []); + + // Assert + result.EquityCurve.ShouldBeEmpty(); + result.Trades.ShouldBeEmpty(); + result.FinalEquity.ShouldBe(50_000.0); + result.Metrics.BarCount.ShouldBe(0); + AssertNoNaN(result.Metrics); + } + + [Theory] + [MemberData(nameof(StrategyNames))] + public void ASeriesShorterThanTheLongestLookbackNeverTradesOnGarbage(string strategyName) + { + // Arrange - 10 bars against an SMA(50) / MACD(26,9) / BBands(20) line-up. + IReadOnlyList bars = TestBars.FromCloses([10, 12, 11, 15, 14, 18, 17, 20, 19, 22]); + BacktestEngine engine = new(new BacktestOptions { InitialCapital = 50_000 }); + + // Act + BacktestResult result = engine.Run(Create(strategyName), bars); + + // Assert + result.EquityCurve.Count.ShouldBe(10); + result.EquityCurve.ShouldAllBe(point => double.IsFinite(point.Equity)); + AssertNoNaN(result.Metrics); + + // Only buy-and-hold, which uses no indicator, is allowed to trade this early. + if (strategyName != nameof(BuyAndHoldStrategy)) + { + result.Trades.ShouldBeEmpty(); + result.FinalEquity.ShouldBe(50_000.0, 1e-9); + } + } + + [Theory] + [MemberData(nameof(StrategyNames))] + public void ASingleBarProducesOneEquityPointAndNoTrade(string strategyName) + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([100.0]); + BacktestEngine engine = new(new BacktestOptions { InitialCapital = 50_000 }); + + // Act + BacktestResult result = engine.Run(Create(strategyName), bars); + + // Assert - there is no "next bar" to fill against, so nothing can ever be executed. + result.EquityCurve.Count.ShouldBe(1); + result.Trades.ShouldBeEmpty(); + result.FinalEquity.ShouldBe(50_000.0); + result.Metrics.Cagr.ShouldBe(0.0); + result.Metrics.AnnualizedVolatility.ShouldBe(0.0); + AssertNoNaN(result.Metrics); + } + + [Theory] + [MemberData(nameof(StrategyNames))] + public void AZeroVolatilitySeriesProducesZeroRiskStatisticsRatherThanNaN(string strategyName) + { + // Arrange - 300 identical bars: every return, every range and every standard deviation is zero. + IReadOnlyList bars = TestBars.Flat(300, 100.0); + BacktestOptions options = new() + { + InitialCapital = 50_000, + CommissionBps = 0, + SlippageBps = 0 + }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(Create(strategyName), bars); + + // Assert - with no costs and no price movement the account cannot change value. + result.FinalEquity.ShouldBe(50_000.0, 1e-6); + result.Metrics.AnnualizedVolatility.ShouldBe(0.0); + result.Metrics.Sharpe.ShouldBe(0.0); + result.Metrics.Sortino.ShouldBe(0.0); + result.Metrics.Calmar.ShouldBe(0.0); + result.Metrics.MaxDrawdown.ShouldBe(0.0); + AssertNoNaN(result.Metrics); + } + + [Fact] + public void ATwoBarSeriesCanStillFillTheFirstSignal() + { + // Arrange + IReadOnlyList bars = TestBars.FromOpenClose([(100, 100), (100, 120)]); + BacktestOptions options = new() { InitialCapital = 10_000, CommissionBps = 0, SlippageBps = 0 }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(new BuyAndHoldStrategy(), bars); + + // Assert - bought at the open of bar 1 (100) and liquidated at its close (120). + result.Trades.Count.ShouldBe(1); + result.Trades[0].EntryIndex.ShouldBe(1); + result.Trades[0].ExitIndex.ShouldBe(1); + result.Trades[0].BarsHeld.ShouldBe(0); + result.FinalEquity.ShouldBe(12_000.0, 1e-9); + } + + [Fact] + public void APriceCollapseCannotDriveEquityBelowZero() + { + // Arrange - the market loses 99.9% in one bar while the account is fully invested and long only. + IReadOnlyList bars = TestBars.FromOpenClose([(100, 100), (100, 100), (0.1, 0.1), (0.1, 0.1)]); + BacktestOptions options = new() { InitialCapital = 10_000, CommissionBps = 10, SlippageBps = 5 }; + + // Act + BacktestResult result = new BacktestEngine(options).Run(new BuyAndHoldStrategy(), bars); + + // Assert + result.EquityCurve.ShouldAllBe(point => point.Equity >= 0.0); + result.FinalEquity.ShouldBeGreaterThan(0.0); + result.Metrics.MaxDrawdown.ShouldBeGreaterThan(0.99); + AssertNoNaN(result.Metrics); + } + + private static void AssertNoNaN(PerformanceMetrics metrics) + { + double[] values = + [ + metrics.TotalReturn, + metrics.Cagr, + metrics.AnnualizedVolatility, + metrics.MaxDrawdown, + metrics.Sharpe, + metrics.Sortino, + metrics.Calmar, + metrics.WinRate, + metrics.AverageWin, + metrics.AverageLoss, + metrics.Expectancy, + metrics.Exposure, + metrics.FinalEquity + ]; + + values.ShouldAllBe(value => !double.IsNaN(value)); + + // ProfitFactor is the one metric allowed to be infinite (wins and no losses); it may never be NaN. + double.IsNaN(metrics.ProfitFactor).ShouldBeFalse(); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/ExecutionCostTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/ExecutionCostTests.cs new file mode 100644 index 00000000..dc5a67d2 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/ExecutionCostTests.cs @@ -0,0 +1,270 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +/// +/// Checks the fill and cost model of a single, fully hand-computable round trip. +/// +/// +/// The fixture is three bars. A long signal is emitted on bar 0, filled at the open of bar 1 (100), +/// and the engine liquidates at the close of bar 2 (110). With capital C, slippage s and +/// commission c the closed-form final equity is +/// +/// entry fill = 100 * (1 + s) +/// quantity = C / (entry fill * (1 + c)) -> the cash leg, commission included, consumes exactly C +/// exit fill = 110 * (1 - s) +/// final = quantity * exit fill * (1 - c) +/// = C * (110 * (1 - s) * (1 - c)) / (100 * (1 + s) * (1 + c)) +/// +/// +public class ExecutionCostTests +{ + private const double Capital = 100_000.0; + + private static readonly IReadOnlyList s_threeBars = + TestBars.FromOpenClose([(99, 99.5), (100, 105), (105, 110)]); + + private static BacktestResult RunRoundTrip(double commissionBps, double slippageBps) + { + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = commissionBps, + SlippageBps = slippageBps, + PositionFraction = 1.0 + }; + + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary { [0] = Signal.EnterLong }); + return new BacktestEngine(options).Run(strategy, s_threeBars); + } + + [Fact] + public void WithoutFrictionsTheRoundTripEarnsExactlyTheUnderlyingMove() + { + // Arrange / Act - buy at 100, sell at 110, no costs: the account must grow by exactly 10%. + BacktestResult result = RunRoundTrip(commissionBps: 0, slippageBps: 0); + + // Assert + result.Trades.Count.ShouldBe(1); + result.Trades[0].EntryPrice.ShouldBe(100.0, 1e-12); + result.Trades[0].ExitPrice.ShouldBe(110.0, 1e-12); + result.Trades[0].Quantity.ShouldBe(1_000.0, 1e-9); + result.Trades[0].Commission.ShouldBe(0.0, 1e-12); + result.Trades[0].NetProfit.ShouldBe(10_000.0, 1e-9); + result.FinalEquity.ShouldBe(110_000.0, 1e-9); + result.Metrics.TotalReturn.ShouldBe(0.10, 1e-12); + } + + [Fact] + public void SlippageAloneMovesBothFillsAgainstTheAccountByTheExactAmount() + { + // Arrange - 25 bp of slippage, no commission. + const double SlippageRate = 25.0 / 10_000.0; + + // Act + BacktestResult result = RunRoundTrip(commissionBps: 0, slippageBps: 25); + + // Assert + double expectedEntry = 100.0 * (1.0 + SlippageRate); + double expectedExit = 110.0 * (1.0 - SlippageRate); + double expectedFinal = Capital * expectedExit / expectedEntry; + + result.Trades[0].EntryPrice.ShouldBe(expectedEntry, 1e-12); + result.Trades[0].ExitPrice.ShouldBe(expectedExit, 1e-12); + result.Trades[0].Commission.ShouldBe(0.0, 1e-12); + result.FinalEquity.ShouldBe(expectedFinal, 1e-8); + + // 25 bp on each of the two fills costs the account 2s / (1 + s) of its frictionless result, + // which is just under the 50 bp a naive "two times 25 bp" estimate would suggest. + const double FrictionlessFinal = 110_000.0; + double drag = 1.0 - (result.FinalEquity / FrictionlessFinal); + drag.ShouldBe(2.0 * SlippageRate / (1.0 + SlippageRate), 1e-12); + drag.ShouldBeLessThan(2.0 * SlippageRate); + } + + [Fact] + public void CommissionAloneIsChargedOnBothFillsAtTheFilledNotional() + { + // Arrange - 30 bp of commission, no slippage. + const double CommissionRate = 30.0 / 10_000.0; + + // Act + BacktestResult result = RunRoundTrip(commissionBps: 30, slippageBps: 0); + + // Assert + double expectedQuantity = Capital / (100.0 * (1.0 + CommissionRate)); + double expectedCommission = (expectedQuantity * 100.0 * CommissionRate) + (expectedQuantity * 110.0 * CommissionRate); + double expectedFinal = expectedQuantity * 110.0 * (1.0 - CommissionRate); + + result.Trades[0].Quantity.ShouldBe(expectedQuantity, 1e-9); + result.Trades[0].Commission.ShouldBe(expectedCommission, 1e-8); + result.Trades[0].GrossProfit.ShouldBe(expectedQuantity * 10.0, 1e-8); + result.Trades[0].NetProfit.ShouldBe((expectedQuantity * 10.0) - expectedCommission, 1e-8); + result.FinalEquity.ShouldBe(expectedFinal, 1e-8); + } + + [Fact] + public void CommissionAndSlippageTogetherMatchTheClosedFormFinalEquity() + { + // Arrange + const double CommissionRate = 20.0 / 10_000.0; + const double SlippageRate = 10.0 / 10_000.0; + + // Act + BacktestResult result = RunRoundTrip(commissionBps: 20, slippageBps: 10); + + // Assert + double entryFill = 100.0 * (1.0 + SlippageRate); + double exitFill = 110.0 * (1.0 - SlippageRate); + double quantity = Capital / (entryFill * (1.0 + CommissionRate)); + double expectedFinal = quantity * exitFill * (1.0 - CommissionRate); + + result.FinalEquity.ShouldBe(expectedFinal, 1e-8); + result.FinalEquity.ShouldBe( + Capital * (110.0 * (1.0 - SlippageRate) * (1.0 - CommissionRate)) / (100.0 * (1.0 + SlippageRate) * (1.0 + CommissionRate)), + 1e-8); + + // And it is strictly worse than the frictionless run. + result.FinalEquity.ShouldBeLessThan(110_000.0); + } + + [Fact] + public void CashNeverGoesNegativeWhenFullyInvested() + { + // Arrange - sizing consumes the whole account, commission included. + BacktestResult result = RunRoundTrip(commissionBps: 50, slippageBps: 25); + + // Assert + result.EquityCurve[1].Cash.ShouldBe(0.0, 1e-8); + result.EquityCurve.ShouldAllBe(point => point.Cash >= -1e-8); + } + + [Fact] + public void AShortRoundTripEarnsTheInverseMoveAndPaysTheSameCosts() + { + // Arrange - price rises from 100 to 110, so a short must lose about 10%. + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = 0, + SlippageBps = 0, + AllowShort = true + }; + + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary { [0] = Signal.EnterShort }); + + // Act + BacktestResult result = new BacktestEngine(options).Run(strategy, s_threeBars); + + // Assert + result.Trades.Count.ShouldBe(1); + result.Trades[0].Side.ShouldBe(OrderSide.Sell); + result.Trades[0].Quantity.ShouldBe(1_000.0, 1e-9); + result.Trades[0].GrossProfit.ShouldBe(-10_000.0, 1e-9); + result.FinalEquity.ShouldBe(90_000.0, 1e-9); + } + + [Fact] + public void AShortSignalIsDowngradedToFlatWhenShortingIsDisabled() + { + // Arrange + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = 0, + SlippageBps = 0, + AllowShort = false + }; + + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary + { + [0] = Signal.EnterLong, + [1] = Signal.EnterShort + }); + + // Act + BacktestResult result = new BacktestEngine(options).Run(strategy, s_threeBars); + + // Assert - the long is closed at the open of bar 2 and the account stays flat, never short. + result.Trades.Count.ShouldBe(1); + result.Trades[0].Side.ShouldBe(OrderSide.Buy); + result.Trades[0].ExitIndex.ShouldBe(2); + result.EquityCurve.ShouldAllBe(point => point.SignedQuantity >= 0.0); + } + + [Fact] + public void FixedCashSizingCommitsTheConfiguredNotionalOnly() + { + // Arrange + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = 0, + SlippageBps = 0, + Sizing = PositionSizing.FixedCash, + PositionCash = 25_000.0 + }; + + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary { [0] = Signal.EnterLong }); + + // Act + BacktestResult result = new BacktestEngine(options).Run(strategy, s_threeBars); + + // Assert - 25 000 / 100 = 250 units, so the 10-point move earns 2 500. + result.Trades[0].Quantity.ShouldBe(250.0, 1e-9); + result.FinalEquity.ShouldBe(102_500.0, 1e-9); + result.EquityCurve[1].Cash.ShouldBe(75_000.0, 1e-9); + } + + [Fact] + public void ReversingFromLongToShortClosesAndReopensAtTheSameFill() + { + // Arrange + IReadOnlyList bars = TestBars.FromOpenClose([(100, 100), (100, 100), (120, 120), (120, 120)]); + BacktestOptions options = new() + { + InitialCapital = Capital, + CommissionBps = 0, + SlippageBps = 0, + AllowShort = true + }; + + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary + { + [0] = Signal.EnterLong, + [1] = Signal.EnterShort + }); + + // Act + BacktestResult result = new BacktestEngine(options).Run(strategy, bars); + + // Assert - the long is closed at 120 on bar 2 and the short opens at 120 on the same bar. + result.Trades.Count.ShouldBe(2); + result.Trades[0].Side.ShouldBe(OrderSide.Buy); + result.Trades[0].ExitIndex.ShouldBe(2); + result.Trades[0].ExitPrice.ShouldBe(120.0, 1e-12); + result.Trades[1].Side.ShouldBe(OrderSide.Sell); + result.Trades[1].EntryIndex.ShouldBe(2); + result.Trades[1].EntryPrice.ShouldBe(120.0, 1e-12); + } + + [Fact] + public void RepeatingTheSameEntrySignalDoesNotChurnThePosition() + { + // Arrange + BacktestOptions options = new() { InitialCapital = Capital, CommissionBps = 50, SlippageBps = 25 }; + ScriptedStrategy strategy = new((_, _) => Signal.EnterLong); + + // Act + BacktestResult result = new BacktestEngine(options).Run(strategy, s_threeBars); + + // Assert - one entry on bar 1 and one liquidation at the end, not one round trip per bar. + result.Trades.Count.ShouldBe(1); + result.Trades[0].EntryIndex.ShouldBe(1); + result.Trades[0].ExitIndex.ShouldBe(2); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/IndicatorSeriesAlignmentTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/IndicatorSeriesAlignmentTests.cs new file mode 100644 index 00000000..d8421226 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/IndicatorSeriesAlignmentTests.cs @@ -0,0 +1,173 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +/// +/// Guards the TA-Lib alignment rule: output[k] describes bar BegIdx + k, and elements from +/// NBElement onwards are meaningless. Reading the raw array as if element k described bar +/// k shifts every signal in time, which is the classic silent bug when consuming the library. +/// +public class IndicatorSeriesAlignmentTests +{ + [Fact] + public void RawTaLibOutputIsOffsetByBegIdxAndTheSeriesUndoesIt() + { + // Arrange - closes 1..6; SMA(3) is 2 on bar 2, 3 on bar 3, 4 on bar 4, 5 on bar 5 (hand-computed). + double[] closes = [1, 2, 3, 4, 5, 6]; + + // Act + SmaResult raw = TAMath.Sma(0, closes.Length - 1, closes, 3); + IndicatorSeries series = new("SMA(3)", closes.Length, raw.BegIdx, raw.NBElement, raw.Real, window: null); + + // Assert - TA-Lib fills from element 0 and reports where element 0 belongs. + raw.RetCode.ShouldBe(RetCode.Success); + raw.BegIdx.ShouldBe(2); + raw.NBElement.ShouldBe(4); + raw.Real[0].ShouldBe(2.0, 1e-12); + + // The wrapper puts element 0 on bar 2 ... + series.HasValueAt(0).ShouldBeFalse(); + series.HasValueAt(1).ShouldBeFalse(); + series[2].ShouldBe(2.0, 1e-12); + series[3].ShouldBe(3.0, 1e-12); + series[4].ShouldBe(4.0, 1e-12); + series[5].ShouldBe(5.0, 1e-12); + + // ... and the naive read (raw.Real[barIndex]) would have reported 4 on bar 2: a two-bar shift. + raw.Real[2].ShouldBe(4.0, 1e-12); + series[2].ShouldNotBe(raw.Real[2]); + } + + [Fact] + public void ElementsBeyondNbElementAreNeverExposed() + { + // Arrange - the output array is sized endIdx - startIdx + 1 = 6 but only 4 elements are meaningful. + double[] closes = [1, 2, 3, 4, 5, 6]; + SmaResult raw = TAMath.Sma(0, closes.Length - 1, closes, 3); + + // Act + IndicatorSeries series = new("SMA(3)", closes.Length, raw.BegIdx, raw.NBElement, raw.Real, window: null); + + // Assert - bar 2 + 4 = 6 would be the next bar; it does not exist, and nothing beyond bar 5 has a value. + raw.Real.Length.ShouldBe(6); + raw.Real[4].ShouldBe(0.0); + series.Count.ShouldBe(6); + series.NBElement.ShouldBe(4); + Enumerable.Range(0, 6).Count(series.HasValueAt).ShouldBe(4); + } + + [Fact] + public void IndicatorSetProducesTheSameAlignmentAsTheRawCall() + { + // Arrange + double[] closes = [10, 12, 11, 15, 14, 18, 17, 20]; + IReadOnlyList bars = TestBars.FromCloses(closes); + + // Act + IndicatorSet indicators = new(bars, window: null); + IndicatorSeries sma = indicators.Sma(4); + SmaResult raw = TAMath.Sma(0, closes.Length - 1, closes, 4); + + // Assert + sma.BegIdx.ShouldBe(raw.BegIdx); + sma.NBElement.ShouldBe(raw.NBElement); + for (int k = 0; k < raw.NBElement; k++) + { + sma[raw.BegIdx + k].ShouldBe(raw.Real[k], 1e-12); + } + + // Bar 3 is the first with a value: mean of 10, 12, 11, 15 = 12. + sma.BegIdx.ShouldBe(3); + sma[3].ShouldBe(12.0, 1e-12); + } + + [Fact] + public void RequestingTheSameIndicatorTwiceReturnsTheCachedInstance() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([1, 2, 3, 4, 5, 6, 7, 8]); + IndicatorSet indicators = new(bars, window: null); + + // Act + IndicatorSeries first = indicators.Sma(3); + IndicatorSeries second = indicators.Sma(3); + IndicatorSeries other = indicators.Sma(4); + + // Assert + second.ShouldBeSameAs(first); + other.ShouldNotBeSameAs(first); + } + + [Fact] + public void ReadingAWarmUpBarThrowsButTryGetValueReportsItQuietly() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([1, 2, 3, 4, 5]); + IndicatorSet indicators = new(bars, window: null); + IndicatorSeries sma = indicators.Sma(3); + + // Act + bool hasValue = sma.TryGetValue(1, out double value); + + // Assert + hasValue.ShouldBeFalse(); + value.ShouldBe(0.0); + Should.Throw(() => sma[1]); + } + + [Fact] + public void TryGetPairNeedsBothBarsToCarryAValue() + { + // Arrange + IReadOnlyList bars = TestBars.FromCloses([1, 2, 3, 4, 5]); + IndicatorSet indicators = new(bars, window: null); + IndicatorSeries sma = indicators.Sma(3); + + // Act + bool atFirstValidBar = sma.TryGetPair(2, out _, out _); + bool atSecondValidBar = sma.TryGetPair(3, out double previous, out double current); + + // Assert - bar 2 is the first with a value, so the pair (1, 2) is incomplete. + atFirstValidBar.ShouldBeFalse(); + atSecondValidBar.ShouldBeTrue(); + previous.ShouldBe(2.0, 1e-12); + current.ShouldBe(3.0, 1e-12); + } + + [Fact] + public void AFailedTaLibCallProducesASeriesWithNoValues() + { + // Arrange - a period longer than the series makes TA-Lib return nothing usable. + IReadOnlyList bars = TestBars.FromCloses([1, 2, 3]); + IndicatorSet indicators = new(bars, window: null); + + // Act + IndicatorSeries sma = indicators.Sma(50); + + // Assert + sma.NBElement.ShouldBe(0); + Enumerable.Range(0, 3).ShouldAllBe(i => !sma.HasValueAt(i)); + } + + [Fact] + public void AnEmptySeriesProducesAnEmptyIndicator() + { + // Arrange + IReadOnlyList bars = []; + IndicatorSet indicators = new(bars, window: null); + + // Act + IndicatorSeries sma = indicators.Sma(3); + MacdSeries macd = indicators.Macd(12, 26, 9); + BollingerBandSeries bands = indicators.BollingerBands(20, 2.0, 2.0); + + // Assert + sma.Count.ShouldBe(0); + macd.Line.Count.ShouldBe(0); + bands.Upper.Count.ShouldBe(0); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/NoLookAheadTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/NoLookAheadTests.cs new file mode 100644 index 00000000..bb4e7bf6 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/NoLookAheadTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +/// +/// The headline guarantee: a strategy cannot read data from the future, and the engine cannot act on a signal +/// before the bar that follows the one that produced it. +/// +public class NoLookAheadTests +{ + private static readonly IReadOnlyList s_series = + TestBars.FromCloses([10, 11, 9, 12, 13, 11, 14, 15, 13, 16, 17, 15]); + + [Fact] + public void AStrategyThatReadsTomorrowsBarMakesTheRunFail() + { + // Arrange + BacktestEngine engine = new(); + FutureBarPeekingStrategy cheater = new(); + + // Act + LookAheadException exception = Should.Throw(() => engine.Run(cheater, s_series)); + + // Assert + exception.Message.ShouldContain("Look-ahead bias detected"); + exception.Message.ShouldContain("bars"); + } + + [Fact] + public void AStrategyThatReadsAFutureIndicatorValueMakesTheRunFail() + { + // Arrange + BacktestEngine engine = new(); + FutureIndicatorPeekingStrategy cheater = new(); + + // Act + LookAheadException exception = Should.Throw(() => engine.Run(cheater, s_series)); + + // Assert + exception.Message.ShouldContain("SMA(3)"); + } + + [Fact] + public void AStrategyThatProbesForwardIsStoppedOnTheFirstStep() + { + // Arrange + BacktestEngine engine = new(); + SeriesLengthProbingStrategy cheater = new(); + + // Act / Assert - it never reaches an ArgumentOutOfRangeException at the end of the array, + // because the very first step past the current bar is refused. + Should.Throw(() => engine.Run(cheater, s_series)); + } + + [Fact] + public void TheEngineNeverSwallowsALookAheadException() + { + // Arrange - peeking two bars ahead is still peeking. + BacktestEngine engine = new(); + FutureBarPeekingStrategy cheater = new(2); + + // Act / Assert + Should.Throw(() => engine.Run(cheater, s_series)); + } + + [Fact] + public void TheWindowShowsExactlyOneMoreBarOnEveryStepAndNeverRevealsTheTotalLength() + { + // Arrange + BacktestEngine engine = new(); + ScriptedStrategy strategy = new((_, _) => Signal.Hold); + + // Act + engine.Run(strategy, s_series); + + // Assert + strategy.ObservedIndices.ShouldBe([.. Enumerable.Range(0, s_series.Count)]); + strategy.ObservedCounts.ShouldBe([.. Enumerable.Range(1, s_series.Count)]); + } + + [Fact] + public void AnIndicatorSeriesNeverRevealsTheTotalLengthOfTheSeries() + { + // Arrange - the window guard covers IBarWindow, but a strategy also holds the IndicatorSeries objects + // it asked for in Initialize. Reading Count off one of those used to hand back the full bar count, + // which is enough to express an end-of-sample bias without ever reading a price. + BacktestEngine engine = new(); + IndicatorLengthProbingStrategy cheater = new(); + + // Act + engine.Run(cheater, s_series); + + // Assert - nothing observed, at any point, equals the total number of bars. + cheater.CountAtInitialize.ShouldBe(0); + cheater.Observed.Count.ShouldBe(s_series.Count); + cheater.Observed.Select(o => o.Count).ShouldBe([.. Enumerable.Range(1, s_series.Count)]); + + // SMA(3) starts on bar 2, so BegIdx stays hidden until the window reaches it, and NBElement only ever + // counts values that have already happened. BegIdx + NBElement is therefore never past the cursor. + cheater.Observed[0].BegIdx.ShouldBe(-1); + cheater.Observed[1].BegIdx.ShouldBe(-1); + cheater.Observed[2].BegIdx.ShouldBe(2); + cheater.Observed[0].NBElement.ShouldBe(0); + cheater.Observed[2].NBElement.ShouldBe(1); + for (int i = 0; i < cheater.Observed.Count; i++) + { + (_, int begIdx, int nbElement) = cheater.Observed[i]; + if (begIdx >= 0) + { + (begIdx + nbElement).ShouldBeLessThanOrEqualTo(i + 1); + } + } + } + + [Fact] + public void ProbingAnIndicatorPastTheEndOfTheSeriesThrowsInsteadOfAnsweringQuietly() + { + // Arrange - TryGetPair used to range-check against the full length, so a probe past the end returned + // false while a probe merely in the future threw. The pair of answers located the end of the series. + BacktestEngine engine = new(); + IndicatorPairProbingStrategy cheater = new(); + + // Act / Assert + Should.Throw(() => engine.Run(cheater, s_series)); + } + + [Fact] + public void ASignalTakenOnBarZeroIsFilledAtTheOpenOfBarOne() + { + // Arrange - zero frictions so the fill price is exactly the bar's open. + IReadOnlyList bars = TestBars.FromOpenClose([(100, 101), (103, 105), (106, 108)]); + BacktestOptions options = new() + { + InitialCapital = 10_000, + CommissionBps = 0, + SlippageBps = 0 + }; + + BacktestEngine engine = new(options); + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary { [0] = Signal.EnterLong }); + + // Act + BacktestResult result = engine.Run(strategy, bars); + + // Assert - the fill is on bar 1 at 103, never on bar 0 at 100 or 101. + result.Trades.Count.ShouldBe(1); + result.Trades[0].EntryIndex.ShouldBe(1); + result.Trades[0].EntryPrice.ShouldBe(103.0, 1e-12); + + // Bar 0 closes with the account still flat and untouched. + result.EquityCurve[0].SignedQuantity.ShouldBe(0.0); + result.EquityCurve[0].Equity.ShouldBe(10_000.0, 1e-12); + } + + [Fact] + public void ASignalEmittedOnTheLastBarIsNeverExecuted() + { + // Arrange + IReadOnlyList bars = TestBars.FromOpenClose([(100, 101), (101, 102), (102, 103)]); + BacktestEngine engine = new(new BacktestOptions { CommissionBps = 0, SlippageBps = 0 }); + ScriptedStrategy strategy = ScriptedStrategy.At(new Dictionary { [2] = Signal.EnterLong }); + + // Act + BacktestResult result = engine.Run(strategy, bars); + + // Assert - there is no bar 3 to fill against, so nothing happened. + result.Trades.ShouldBeEmpty(); + result.FinalEquity.ShouldBe(result.InitialCapital, 1e-12); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/TradeAndPositionTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/TradeAndPositionTests.cs new file mode 100644 index 00000000..ccd3b088 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Engine/TradeAndPositionTests.cs @@ -0,0 +1,136 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Engine; + +public class TradeAndPositionTests +{ + [Fact] + public void ALongTradeProfitsFromARisingExit() + { + // Arrange - 10 units bought at 100 and sold at 112, with 5 of total commission. + Trade trade = new(OrderSide.Buy, 10.0, 0, TestBars.Origin, 100.0, 5, TestBars.Origin.AddDays(5), 112.0, 5.0); + + // Act / Assert + trade.IsLong.ShouldBeTrue(); + trade.GrossProfit.ShouldBe(120.0, 1e-12); + trade.NetProfit.ShouldBe(115.0, 1e-12); + trade.ReturnOnNotional.ShouldBe(115.0 / 1_000.0, 1e-12); + trade.BarsHeld.ShouldBe(5); + trade.IsWin.ShouldBeTrue(); + trade.IsLoss.ShouldBeFalse(); + } + + [Fact] + public void AShortTradeProfitsFromAFallingExit() + { + // Arrange - 10 units sold at 100 and bought back at 88. + Trade trade = new(OrderSide.Sell, 10.0, 0, TestBars.Origin, 100.0, 3, TestBars.Origin.AddDays(3), 88.0, 5.0); + + // Act / Assert + trade.IsLong.ShouldBeFalse(); + trade.GrossProfit.ShouldBe(120.0, 1e-12); + trade.NetProfit.ShouldBe(115.0, 1e-12); + trade.IsWin.ShouldBeTrue(); + } + + [Fact] + public void ABreakEvenTradeIsNeitherAWinNorALoss() + { + // Arrange - the whole gross profit is eaten by commission. + Trade trade = new(OrderSide.Buy, 10.0, 0, TestBars.Origin, 100.0, 1, TestBars.Origin.AddDays(1), 110.0, 100.0); + + // Act / Assert + trade.NetProfit.ShouldBe(0.0, 1e-12); + trade.IsWin.ShouldBeFalse(); + trade.IsLoss.ShouldBeFalse(); + } + + [Fact] + public void AZeroNotionalTradeReportsAZeroReturnRatherThanNaN() + { + // Arrange + Trade trade = new(OrderSide.Buy, 0.0, 0, TestBars.Origin, 100.0, 1, TestBars.Origin.AddDays(1), 110.0, 0.0); + + // Act / Assert + trade.ReturnOnNotional.ShouldBe(0.0); + } + + [Fact] + public void ALongPositionHasPositiveSignedQuantity() + { + // Arrange + Position position = new(OrderSide.Buy, 25.0, 40.0, 3, TestBars.Origin, 1.0); + + // Act / Assert + position.IsLong.ShouldBeTrue(); + position.IsShort.ShouldBeFalse(); + position.SignedQuantity.ShouldBe(25.0); + position.MarketValue(50.0).ShouldBe(1_250.0, 1e-12); + position.UnrealizedProfit(50.0).ShouldBe(250.0, 1e-12); + } + + [Fact] + public void AShortPositionHasNegativeSignedQuantityAndInvertedProfit() + { + // Arrange + Position position = new(OrderSide.Sell, 25.0, 40.0, 3, TestBars.Origin, 1.0); + + // Act / Assert + position.IsShort.ShouldBeTrue(); + position.SignedQuantity.ShouldBe(-25.0); + position.MarketValue(50.0).ShouldBe(-1_250.0, 1e-12); + position.UnrealizedProfit(50.0).ShouldBe(-250.0, 1e-12); + position.UnrealizedProfit(30.0).ShouldBe(250.0, 1e-12); + } + + [Fact] + public void AnEquityPointKnowsWhetherItWasExposed() + { + // Arrange + EquityPoint flat = new(0, TestBars.Origin, 100.0, 1_000.0, 0.0, 1_000.0); + EquityPoint longPoint = new(1, TestBars.Origin.AddDays(1), 100.0, 0.0, 10.0, 1_000.0); + EquityPoint shortPoint = new(2, TestBars.Origin.AddDays(2), 100.0, 2_000.0, -10.0, 1_000.0); + + // Act / Assert + flat.IsInPosition.ShouldBeFalse(); + longPoint.IsInPosition.ShouldBeTrue(); + shortPoint.IsInPosition.ShouldBeTrue(); + } + + [Theory] + [InlineData(100, 105, 95, 102, 1000, true)] + [InlineData(100, 99, 95, 98, 1000, false)] + [InlineData(100, 105, 101, 102, 1000, false)] + [InlineData(100, 105, 95, 102, -1, false)] + [InlineData(-100, 105, 95, 102, 1000, false)] + [InlineData(100, double.NaN, 95, 102, 1000, false)] + public void ABarKnowsWhetherItIsInternallyConsistent( + double open, + double high, + double low, + double close, + double volume, + bool expected) + { + // Arrange + Bar bar = new(TestBars.Origin, open, high, low, close, volume); + + // Act / Assert + bar.IsWellFormed().ShouldBe(expected); + } + + [Fact] + public void ABarExposesItsTypicalPriceAndRange() + { + // Arrange + Bar bar = new(TestBars.Origin, 100.0, 110.0, 90.0, 104.0, 1_000.0); + + // Act / Assert + bar.TypicalPrice.ShouldBe((110.0 + 90.0 + 104.0) / 3.0, 1e-12); + bar.Range.ShouldBe(20.0, 1e-12); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/GlobalUsings.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/GlobalUsings.cs new file mode 100644 index 00000000..a8e14c0a --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/GlobalUsings.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +global using System.Globalization; +global using Shouldly; +global using TechnicalAnalysis.Common; +global using TechnicalAnalysis.Functions; +global using TechnicalAnalysis.Samples.Backtesting; +global using TechnicalAnalysis.Samples.Backtesting.Data; +global using TechnicalAnalysis.Samples.Backtesting.Engine; +global using TechnicalAnalysis.Samples.Backtesting.Metrics; +global using TechnicalAnalysis.Samples.Backtesting.Reporting; +global using TechnicalAnalysis.Samples.Backtesting.Strategies; +global using TechnicalAnalysis.Samples.Backtesting.UnitTests.TestSupport; +global using Xunit; + +// Same name collision as the sample project: pin the engine's types. +global using IndicatorSeries = TechnicalAnalysis.Samples.Backtesting.Engine.IndicatorSeries; +global using MacdSeries = TechnicalAnalysis.Samples.Backtesting.Engine.MacdSeries; diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Metrics/PerformanceMetricsTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Metrics/PerformanceMetricsTests.cs new file mode 100644 index 00000000..defc702f --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Metrics/PerformanceMetricsTests.cs @@ -0,0 +1,445 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Metrics; + +/// +/// Every formula is checked against a fixture small enough to be computed by hand in the comments. +/// +/// +/// +/// The reference equity curve is [100, 110, 99, 108.9] (four bars, three returns): +/// +/// +/// r = [+0.10, -0.10, +0.10] +/// mean(r) = 0.10 / 3 = 0.0333333... +/// var(r) = ((0.0666667)^2 + (-0.1333333)^2 + (0.0666667)^2) / 2 = 0.01333333... +/// stdev(r) = sqrt(0.0133333...) = 0.115470053837925 +/// downside = sqrt((0 + 0.01 + 0) / 3) = 0.057735026918963 +/// maxDD = (110 - 99) / 110 = 0.10 +/// ddBars = peak at bar 1, still under water at bar 3 -> 3 - 1 = 2 +/// +/// +/// Annualisation constant: 252 bars per year, so the scale factor is sqrt(252) = 15.874507866... +/// and the horizon is (4 - 1) / 252 years. A tolerance of 1e-9 is used throughout: the +/// quantities are O(1)–O(10), so that is roughly seven orders of magnitude above double rounding noise while +/// still catching any real formula change. +/// +/// +public class PerformanceMetricsTests +{ + private const double Tolerance = 1e-9; + private const int BarsPerYear = 252; + + private static readonly double[] s_referenceEquity = [100.0, 110.0, 99.0, 108.9]; + + private static List Curve(double[] equity, bool inPosition = true) + { + List points = new(equity.Length); + for (int i = 0; i < equity.Length; i++) + { + points.Add(new EquityPoint(i, TestBars.Origin.AddDays(i), equity[i], 0.0, inPosition ? 1.0 : 0.0, equity[i])); + } + + return points; + } + + private static PerformanceMetrics Reference() + { + return PerformanceMetrics.Compute(Curve(s_referenceEquity), [], 100.0, BarsPerYear); + } + + [Fact] + public void TotalReturnIsFinalOverInitialMinusOne() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert - 108.9 / 100 - 1 = 0.089 + metrics.FinalEquity.ShouldBe(108.9, Tolerance); + metrics.TotalReturn.ShouldBe(0.089, Tolerance); + } + + [Fact] + public void CagrCompoundsOverBarIntervalsNotOverEquityPoints() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert - four points span three bar intervals, so years = 3 / 252. + double years = 3.0 / BarsPerYear; + double expected = Math.Pow(1.089, 1.0 / years) - 1.0; + + metrics.Cagr.ShouldBe(expected, Tolerance); + metrics.BarsPerYear.ShouldBe(BarsPerYear); + } + + [Fact] + public void AnnualisedVolatilityIsTheSampleStandardDeviationScaledBySqrtBarsPerYear() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert - stdev = sqrt(0.04 / 3) = 0.2 / sqrt(3), so the annualised figure is + // (0.2 / sqrt(3)) * sqrt(252) = 0.2 * sqrt(84) = 1.83303027798... + double sampleStdDev = 0.2 / Math.Sqrt(3.0); + metrics.AnnualizedVolatility.ShouldBe(sampleStdDev * Math.Sqrt(BarsPerYear), Tolerance); + metrics.AnnualizedVolatility.ShouldBe(0.2 * Math.Sqrt(84.0), Tolerance); + } + + [Fact] + public void MaxDrawdownIsThePeakToTroughDeclineAndItsDurationIsMeasuredInBars() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert - the peak is 110 at bar 1, the trough 99 at bar 2, and 108.9 never regains 110. + metrics.PeakEquity.ShouldBe(110.0, Tolerance); + metrics.MaxDrawdown.ShouldBe(11.0 / 110.0, Tolerance); + metrics.MaxDrawdown.ShouldBe(0.10, Tolerance); + metrics.MaxDrawdownDurationBars.ShouldBe(2); + } + + [Fact] + public void MaxDrawdownDurationCountsTheRecoveryBar() + { + // Arrange - 100, 110, 99, 110: the peak is set at bar 1 and regained at bar 3. The documented measure + // runs from the peak bar to the first later bar that reaches it again, so the answer is 3 - 1 = 2. + // Measuring only while under water would stop at bar 2 and report 1 for every recovered drawdown. + double[] equity = [100.0, 110.0, 99.0, 110.0]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(equity), [], 100.0, BarsPerYear); + + // Assert + metrics.MaxDrawdownDurationBars.ShouldBe(2); + metrics.MaxDrawdown.ShouldBe(11.0 / 110.0, Tolerance); + } + + [Fact] + public void AMonotonicallyRisingCurveHasNoDrawdownDuration() + { + // Arrange - a curve that only ever sets new peaks. Counting the length on the recovery branch must + // not mistake "reached a new high" for "recovered from a drawdown". + double[] equity = [100.0, 110.0, 120.0, 130.0]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(equity), [], 100.0, BarsPerYear); + + // Assert + metrics.MaxDrawdown.ShouldBe(0.0); + metrics.MaxDrawdownDurationBars.ShouldBe(0); + } + + [Fact] + public void SharpeIsTheMeanReturnOverItsStandardDeviationAnnualised() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert - mean / stdev = (0.1 / 3) / (0.2 / sqrt(3)) = sqrt(3) / 6, and + // (sqrt(3) / 6) * sqrt(252) = sqrt(84) / 2 = 4.58257569495584. + const double Mean = 0.1 / 3.0; + double sampleStdDev = 0.2 / Math.Sqrt(3.0); + metrics.Sharpe.ShouldBe(Mean / sampleStdDev * Math.Sqrt(BarsPerYear), Tolerance); + metrics.Sharpe.ShouldBe(Math.Sqrt(84.0) / 2.0, Tolerance); + } + + [Fact] + public void SortinoDividesByTheDownsideDeviationTakenOverAllReturns() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert - downside = sqrt((0 + 0.01 + 0) / 3) = 0.0577350269, exactly half the sample stdev here, + // so Sortino must be exactly twice Sharpe for this fixture. + const double Mean = 0.1 / 3.0; + double downside = Math.Sqrt(0.01 / 3.0); + + metrics.Sortino.ShouldBe(Mean / downside * Math.Sqrt(BarsPerYear), Tolerance); + metrics.Sortino.ShouldBe(2.0 * metrics.Sharpe, 1e-9); + } + + [Fact] + public void CalmarIsCagrOverMaxDrawdown() + { + // Arrange / Act + PerformanceMetrics metrics = Reference(); + + // Assert + metrics.Calmar.ShouldBe(metrics.Cagr / 0.10, Tolerance); + } + + [Fact] + public void ARiskFreeRateIsDeAnnualisedGeometricallyBeforeBeingSubtracted() + { + // Arrange - 4% a year over 252 bars. + const double AnnualRate = 0.04; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(s_referenceEquity), [], 100.0, BarsPerYear, AnnualRate); + + // Assert + double perBar = Math.Pow(1.0 + AnnualRate, 1.0 / BarsPerYear) - 1.0; + const double Mean = 0.1 / 3.0; + double sampleStdDev = 0.2 / Math.Sqrt(3.0); + + metrics.RiskFreeRate.ShouldBe(AnnualRate); + metrics.Sharpe.ShouldBe((Mean - perBar) / sampleStdDev * Math.Sqrt(BarsPerYear), Tolerance); + metrics.Sharpe.ShouldBeLessThan(Math.Sqrt(84.0) / 2.0); + } + + [Fact] + public void ExposureCountsTheBarsWithAnOpenPosition() + { + // Arrange - flat on bars 0 and 3, in the market on bars 1 and 2. + List points = + [ + new(0, TestBars.Origin, 100.0, 100.0, 0.0, 100.0), + new(1, TestBars.Origin.AddDays(1), 110.0, 0.0, 1.0, 110.0), + new(2, TestBars.Origin.AddDays(2), 99.0, 0.0, 1.0, 99.0), + new(3, TestBars.Origin.AddDays(3), 108.9, 108.9, 0.0, 108.9) + ]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(points, [], 100.0, BarsPerYear); + + // Assert + metrics.Exposure.ShouldBe(0.5, Tolerance); + metrics.BarCount.ShouldBe(4); + } + + [Fact] + public void TradeStatisticsAreComputedFromNetProfitAfterCommission() + { + // Arrange - four long round trips of 10 units: +200, -100, +100, -50 net. + IReadOnlyList trades = + [ + LongTrade(entry: 100, exit: 120, quantity: 10), + LongTrade(entry: 100, exit: 90, quantity: 10), + LongTrade(entry: 100, exit: 110, quantity: 10), + LongTrade(entry: 100, exit: 95, quantity: 10) + ]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(s_referenceEquity), trades, 100.0, BarsPerYear); + + // Assert - gross profit 300, gross loss 150. + metrics.TradeCount.ShouldBe(4); + metrics.WinCount.ShouldBe(2); + metrics.LossCount.ShouldBe(2); + metrics.WinRate.ShouldBe(0.5, Tolerance); + metrics.ProfitFactor.ShouldBe(2.0, Tolerance); + metrics.AverageWin.ShouldBe(150.0, Tolerance); + metrics.AverageLoss.ShouldBe(-75.0, Tolerance); + metrics.Expectancy.ShouldBe(37.5, Tolerance); + + // The textbook expectancy identity holds when no trade is exactly break-even. + double textbook = (metrics.WinRate * metrics.AverageWin) + ((1.0 - metrics.WinRate) * metrics.AverageLoss); + metrics.Expectancy.ShouldBe(textbook, Tolerance); + } + + [Fact] + public void CommissionIsSubtractedBeforeATradeIsClassified() + { + // Arrange - a 10-point gain on 10 units is +100 gross but -20 net after 120 of commission. + IReadOnlyList trades = [LongTrade(entry: 100, exit: 110, quantity: 10, commission: 120)]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(s_referenceEquity), trades, 100.0, BarsPerYear); + + // Assert + trades[0].GrossProfit.ShouldBe(100.0, Tolerance); + trades[0].NetProfit.ShouldBe(-20.0, Tolerance); + metrics.WinCount.ShouldBe(0); + metrics.LossCount.ShouldBe(1); + metrics.WinRate.ShouldBe(0.0); + metrics.ProfitFactor.ShouldBe(0.0); + metrics.Expectancy.ShouldBe(-20.0, Tolerance); + } + + [Fact] + public void ProfitFactorIsInfiniteWhenThereAreWinsAndNoLosses() + { + // Arrange + IReadOnlyList trades = [LongTrade(entry: 100, exit: 110, quantity: 10)]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(s_referenceEquity), trades, 100.0, BarsPerYear); + + // Assert - documented convention: wins and no losses reads as an infinite profit factor. + metrics.ProfitFactor.ShouldBe(double.PositiveInfinity); + metrics.WinRate.ShouldBe(1.0); + double.IsNaN(metrics.ProfitFactor).ShouldBeFalse(); + } + + [Fact] + public void AnAllLosingSeriesProducesDefinedNegativeStatistics() + { + // Arrange - three losing round trips and a monotonically falling curve. + IReadOnlyList trades = + [ + LongTrade(entry: 100, exit: 90, quantity: 10), + LongTrade(entry: 100, exit: 80, quantity: 10), + LongTrade(entry: 100, exit: 70, quantity: 10) + ]; + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve([100.0, 90.0, 80.0, 70.0]), trades, 100.0, BarsPerYear); + + // Assert + metrics.WinCount.ShouldBe(0); + metrics.LossCount.ShouldBe(3); + metrics.WinRate.ShouldBe(0.0); + metrics.ProfitFactor.ShouldBe(0.0); + metrics.AverageWin.ShouldBe(0.0); + metrics.AverageLoss.ShouldBe(-200.0, Tolerance); + metrics.Expectancy.ShouldBe(-200.0, Tolerance); + metrics.TotalReturn.ShouldBe(-0.30, Tolerance); + metrics.Cagr.ShouldBeLessThan(0.0); + metrics.Sharpe.ShouldBeLessThan(0.0); + metrics.MaxDrawdown.ShouldBe(0.30, Tolerance); + AssertNoNaN(metrics); + } + + [Fact] + public void NoTradesProducesZeroesRatherThanNaN() + { + // Arrange / Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve(s_referenceEquity), [], 100.0, BarsPerYear); + + // Assert + metrics.TradeCount.ShouldBe(0); + metrics.WinRate.ShouldBe(0.0); + metrics.ProfitFactor.ShouldBe(0.0); + metrics.AverageWin.ShouldBe(0.0); + metrics.AverageLoss.ShouldBe(0.0); + metrics.Expectancy.ShouldBe(0.0); + AssertNoNaN(metrics); + } + + [Fact] + public void AFlatCurveHasZeroVolatilityAndZeroRatiosRatherThanNaN() + { + // Arrange - constant equity: every return is zero, so both denominators vanish. + IReadOnlyList curve = Curve([100.0, 100.0, 100.0, 100.0, 100.0]); + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(curve, [], 100.0, BarsPerYear); + + // Assert + metrics.AnnualizedVolatility.ShouldBe(0.0); + metrics.Sharpe.ShouldBe(0.0); + metrics.Sortino.ShouldBe(0.0); + metrics.Calmar.ShouldBe(0.0); + metrics.MaxDrawdown.ShouldBe(0.0); + metrics.MaxDrawdownDurationBars.ShouldBe(0); + metrics.Cagr.ShouldBe(0.0, Tolerance); + AssertNoNaN(metrics); + } + + [Fact] + public void ASingleEquityPointHasNoReturnsAndNoElapsedTime() + { + // Arrange / Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(Curve([100.0]), [], 100.0, BarsPerYear); + + // Assert + metrics.BarCount.ShouldBe(1); + metrics.TotalReturn.ShouldBe(0.0, Tolerance); + metrics.Cagr.ShouldBe(0.0); + metrics.AnnualizedVolatility.ShouldBe(0.0); + metrics.Sharpe.ShouldBe(0.0); + AssertNoNaN(metrics); + } + + [Fact] + public void AnEmptyCurveFallsBackToTheInitialCapital() + { + // Arrange / Act + PerformanceMetrics metrics = PerformanceMetrics.Compute([], [], 100.0, BarsPerYear); + + // Assert + metrics.BarCount.ShouldBe(0); + metrics.FinalEquity.ShouldBe(100.0); + metrics.PeakEquity.ShouldBe(100.0); + metrics.TotalReturn.ShouldBe(0.0); + AssertNoNaN(metrics); + } + + [Fact] + public void AWipedOutAccountReportsATotalLossInsteadOfPoisoningEveryStatistic() + { + // Arrange - equity reaches exactly zero, which would make the next percentage return undefined. + IReadOnlyList curve = Curve([100.0, 50.0, 0.0, 0.0]); + + // Act + PerformanceMetrics metrics = PerformanceMetrics.Compute(curve, [], 100.0, BarsPerYear); + + // Assert + metrics.TotalReturn.ShouldBe(-1.0, Tolerance); + metrics.Cagr.ShouldBe(-1.0); + metrics.MaxDrawdown.ShouldBe(1.0, Tolerance); + AssertNoNaN(metrics); + } + + [Fact] + public void ChangingBarsPerYearRescalesOnlyTheAnnualisedFigures() + { + // Arrange + PerformanceMetrics daily = PerformanceMetrics.Compute(Curve(s_referenceEquity), [], 100.0, 252); + PerformanceMetrics monthly = PerformanceMetrics.Compute(Curve(s_referenceEquity), [], 100.0, 12); + + // Act + double ratio = Math.Sqrt(252.0 / 12.0); + + // Assert - the per-bar statistics are identical; only the annualisation differs. + daily.TotalReturn.ShouldBe(monthly.TotalReturn, Tolerance); + daily.MaxDrawdown.ShouldBe(monthly.MaxDrawdown, Tolerance); + daily.AnnualizedVolatility.ShouldBe(monthly.AnnualizedVolatility * ratio, Tolerance); + daily.Sharpe.ShouldBe(monthly.Sharpe * ratio, Tolerance); + } + + [Fact] + public void ComputeRejectsAnImpossibleConfiguration() + { + // Arrange / Act / Assert + Should.Throw(() => PerformanceMetrics.Compute([], [], 0.0, 252)); + Should.Throw(() => PerformanceMetrics.Compute([], [], 100.0, 0)); + Should.Throw(() => PerformanceMetrics.Compute(null!, [], 100.0, 252)); + } + + private static Trade LongTrade(double entry, double exit, double quantity, double commission = 0.0) + { + return new Trade( + OrderSide.Buy, + quantity, + 0, + TestBars.Origin, + entry, + 1, + TestBars.Origin.AddDays(1), + exit, + commission); + } + + private static void AssertNoNaN(PerformanceMetrics metrics) + { + double.IsNaN(metrics.TotalReturn).ShouldBeFalse(nameof(metrics.TotalReturn)); + double.IsNaN(metrics.Cagr).ShouldBeFalse(nameof(metrics.Cagr)); + double.IsNaN(metrics.AnnualizedVolatility).ShouldBeFalse(nameof(metrics.AnnualizedVolatility)); + double.IsNaN(metrics.MaxDrawdown).ShouldBeFalse(nameof(metrics.MaxDrawdown)); + double.IsNaN(metrics.Sharpe).ShouldBeFalse(nameof(metrics.Sharpe)); + double.IsNaN(metrics.Sortino).ShouldBeFalse(nameof(metrics.Sortino)); + double.IsNaN(metrics.Calmar).ShouldBeFalse(nameof(metrics.Calmar)); + double.IsNaN(metrics.WinRate).ShouldBeFalse(nameof(metrics.WinRate)); + double.IsNaN(metrics.ProfitFactor).ShouldBeFalse(nameof(metrics.ProfitFactor)); + double.IsNaN(metrics.AverageWin).ShouldBeFalse(nameof(metrics.AverageWin)); + double.IsNaN(metrics.AverageLoss).ShouldBeFalse(nameof(metrics.AverageLoss)); + double.IsNaN(metrics.Expectancy).ShouldBeFalse(nameof(metrics.Expectancy)); + double.IsNaN(metrics.Exposure).ShouldBeFalse(nameof(metrics.Exposure)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Reporting/AsciiEquityCurveTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Reporting/AsciiEquityCurveTests.cs new file mode 100644 index 00000000..be19f823 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Reporting/AsciiEquityCurveTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Reporting; + +public class AsciiEquityCurveTests +{ + private static List Curve(params double[] equity) + { + List points = new(equity.Length); + for (int i = 0; i < equity.Length; i++) + { + points.Add(new EquityPoint(i, TestBars.Origin.AddDays(i), equity[i], 0.0, 0.0, equity[i])); + } + + return points; + } + + [Fact] + public void TheChartHasTheRequestedShape() + { + // Arrange + IReadOnlyList curve = Curve(100, 105, 103, 110, 108, 120); + + // Act + string chart = AsciiEquityCurve.Render(curve, width: 40, height: 8); + + // Assert - eight plot rows, one axis row and one date row. + string[] lines = chart.Split(Environment.NewLine); + lines.Length.ShouldBe(10); + lines.Take(8).ShouldAllBe(line => line.Contains('|', StringComparison.Ordinal)); + lines[8].ShouldContain("+---"); + } + + [Fact] + public void TheCurveIsNormalisedSoTheInitialCapitalReadsAsOneHundred() + { + // Arrange - the account doubles. + IReadOnlyList curve = Curve(50_000, 75_000, 100_000); + + // Act + string chart = AsciiEquityCurve.Render(curve, width: 20, height: 6); + + // Assert - the axis labels are the normalised extremes, not the raw currency amounts. + chart.ShouldContain("200.0"); + chart.ShouldContain("100.0"); + chart.ShouldNotContain("50,000"); + } + + [Fact] + public void RisingAndFallingSegmentsArePlottedInTheRightOrder() + { + // Arrange - a strictly rising curve must end higher on the canvas than it starts. + IReadOnlyList curve = Curve(100, 110, 120, 130, 140, 150, 160, 170); + + // Act + string chart = AsciiEquityCurve.Render(curve, width: 8, height: 8); + string[] plotRows = [.. chart.Split(Environment.NewLine).Take(8)]; + + // Assert - row 0 is the top of the chart, so the first plot must sit lower down than the last one. + int firstColumnRow = Array.FindIndex(plotRows, row => row[^8] == '*'); + int lastColumnRow = Array.FindIndex(plotRows, row => row[^1] == '*'); + lastColumnRow.ShouldBeLessThan(firstColumnRow); + } + + [Fact] + public void AFlatCurveDoesNotDivideByZero() + { + // Arrange + IReadOnlyList curve = Curve(100, 100, 100, 100); + + // Act + string chart = AsciiEquityCurve.Render(curve, width: 20, height: 6); + + // Assert + chart.ShouldContain('*'); + chart.ShouldNotContain("NaN"); + chart.Split(Environment.NewLine).Length.ShouldBe(8); + } + + [Fact] + public void ASinglePointCurveRenders() + { + // Arrange + IReadOnlyList curve = Curve(100); + + // Act + string chart = AsciiEquityCurve.Render(curve, width: 10, height: 4); + + // Assert + chart.ShouldContain('*'); + } + + [Fact] + public void AnEmptyCurveRendersAPlaceholder() + { + // Arrange / Act + string chart = AsciiEquityCurve.Render([]); + + // Assert + chart.ShouldContain("no equity curve"); + } + + [Fact] + public void TheAxisCarriesTheFirstAndLastDates() + { + // Arrange + IReadOnlyList curve = Curve(100, 110, 120, 130); + + // Act + string chart = AsciiEquityCurve.Render(curve, width: 40, height: 6); + + // Assert + chart.ShouldContain(TestBars.Origin.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + chart.ShouldContain(TestBars.Origin.AddDays(3).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); + } + + [Theory] + [InlineData(1, 10)] + [InlineData(10, 1)] + public void AnUnusableCanvasSizeIsRejected(int width, int height) + { + // Arrange / Act / Assert + Should.Throw(() => AsciiEquityCurve.Render(Curve(100, 110), width, height)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Reporting/ConsoleReportTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Reporting/ConsoleReportTests.cs new file mode 100644 index 00000000..c156d969 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Reporting/ConsoleReportTests.cs @@ -0,0 +1,197 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Reporting; + +public class ConsoleReportTests +{ + private static readonly IReadOnlyList s_series = SyntheticSeriesGenerator.Generate(400, seed: 5150); + + private static IReadOnlyList Results() + { + BacktestOptions options = new() { InitialCapital = 100_000 }; + return BacktestSampleRunner.RunAll(BacktestSampleRunner.CreateStrategies(), s_series, options); + } + + [Fact] + public void TheComparisonTableHasOneColumnPerStrategyAndOneRowPerMetric() + { + // Arrange + IReadOnlyList results = Results(); + + // Act + string table = ConsoleReport.RenderComparison(results); + + // Assert + foreach (BacktestResult result in results) + { + table.ShouldContain(result.StrategyName); + } + + table.ShouldContain("Total return"); + table.ShouldContain("Max drawdown"); + table.ShouldContain("Sharpe"); + table.ShouldContain("Sortino"); + table.ShouldContain("Calmar"); + table.ShouldContain("Profit factor"); + table.ShouldContain("Expectancy / trade"); + table.ShouldContain("Exposure"); + } + + [Fact] + public void EveryReportLineFitsInTheHouseLineLength() + { + // Arrange + IReadOnlyList results = Results(); + + // Act + string report = ConsoleReport.RenderConfiguration(results[0].Options, s_series.Count, "test series") + + ConsoleReport.RenderStrategyCard(results[0], "description") + + ConsoleReport.RenderTradeLog(results[0]) + + ConsoleReport.RenderComparison(results); + + // Assert - 140 characters is the repository's maximum line length; the report must respect it too. + string[] lines = report.Split(Environment.NewLine); + lines.ShouldAllBe(line => line.Length <= 140); + } + + [Fact] + public void TheBestValueOfEachComparableRowIsMarked() + { + // Arrange + IReadOnlyList results = Results(); + + // Act + string table = ConsoleReport.RenderComparison(results); + string totalReturnRow = table + .Split(Environment.NewLine) + .First(line => line.StartsWith("Total return", StringComparison.Ordinal)); + + // Assert - exactly one cell of the row carries the marker, and it is the largest total return. + totalReturnRow.Count(c => c == '*').ShouldBe(1); + + double best = results.Max(result => result.Metrics.TotalReturn); + string bestCell = (best * 100.0).ToString("F2", CultureInfo.InvariantCulture) + " %"; + totalReturnRow.ShouldContain("* " + bestCell); + } + + [Fact] + public void NumbersAreFormattedWithTheInvariantCultureOnEveryMachine() + { + // Arrange + PerformanceMetrics metrics = PerformanceMetrics.Compute( + [ + new EquityPoint(0, TestBars.Origin, 100.0, 0.0, 0.0, 1_234.5), + new EquityPoint(1, TestBars.Origin.AddDays(1), 100.0, 0.0, 0.0, 1_357.95) + ], + [], + 1_234.5, + 252); + + // Act + string rendered = ConsoleReport.RenderMetrics(metrics); + + // Assert - a decimal point and a comma group separator, never the other way round. + rendered.ShouldContain("1,357.95"); + rendered.ShouldContain("10.00 %"); + } + + [Fact] + public void AnInfiniteProfitFactorIsRenderedReadably() + { + // Arrange - one winning trade and no losing ones. + IReadOnlyList trades = + [ + new(OrderSide.Buy, 10.0, 0, TestBars.Origin, 100.0, 1, TestBars.Origin.AddDays(1), 110.0, 0.0) + ]; + + PerformanceMetrics metrics = PerformanceMetrics.Compute( + [ + new EquityPoint(0, TestBars.Origin, 100.0, 0.0, 0.0, 1_000.0), + new EquityPoint(1, TestBars.Origin.AddDays(1), 110.0, 0.0, 0.0, 1_100.0) + ], + trades, + 1_000.0, + 252); + + // Act + string rendered = ConsoleReport.RenderMetrics(metrics); + + // Assert + metrics.ProfitFactor.ShouldBe(double.PositiveInfinity); + rendered.ShouldContain("inf"); + rendered.ShouldNotContain("NaN"); + rendered.ShouldNotContain("∞"); + } + + [Fact] + public void TheTradeLogListsRoundTripsAndTruncatesLongOnes() + { + // Arrange + IReadOnlyList results = Results(); + BacktestResult busiest = results.MaxBy(result => result.Trades.Count)!; + + // Act + string log = ConsoleReport.RenderTradeLog(busiest, maxTrades: 3); + + // Assert + busiest.Trades.Count.ShouldBeGreaterThan(3); + log.ShouldContain("Net P&L"); + log.ShouldContain("more round trip(s) not shown"); + log.Split(Environment.NewLine) + .Count(line => + line.Contains("LONG", StringComparison.Ordinal) + || line.Contains("SHORT", StringComparison.Ordinal)) + .ShouldBe(3); + } + + [Fact] + public void AStrategyWithNoTradesGetsAnExplicitTradeLog() + { + // Arrange + BacktestResult result = new BacktestEngine().Run(new SmaCrossoverStrategy(20, 50), TestBars.FromCloses([1, 2, 3, 4, 5])); + + // Act + string log = ConsoleReport.RenderTradeLog(result); + + // Assert + log.ShouldContain("no completed round trips"); + } + + [Fact] + public void AnEmptyComparisonIsHandled() + { + // Arrange / Act + string table = ConsoleReport.RenderComparison([]); + + // Assert + table.ShouldBe("(nothing to compare)"); + } + + [Fact] + public void TheConfigurationBlockEchoesTheFrictionsThatWereApplied() + { + // Arrange + BacktestOptions options = new() + { + InitialCapital = 250_000, + CommissionBps = 7.5, + SlippageBps = 1.25, + AllowShort = true, + BarsPerYear = 365 + }; + + // Act + string rendered = ConsoleReport.RenderConfiguration(options, 1_000, "synthetic"); + + // Assert + rendered.ShouldContain("250,000.00"); + rendered.ShouldContain("7.5 bp per fill"); + rendered.ShouldContain("1.25 bp per fill"); + rendered.ShouldContain("365 bars per year"); + rendered.ShouldContain("enabled"); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/BollingerBreakoutStrategyTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/BollingerBreakoutStrategyTests.cs new file mode 100644 index 00000000..7a32c9d8 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/BollingerBreakoutStrategyTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Strategies; + +/// +/// The fixture is 20 identical closes (so the bands collapse onto the price) followed by one breakout bar. +/// +/// +/// On bar 20 the 20-bar window is nineteen 100s and one 110, so +/// mean = 100.5, population variance = (19 * 0.25 + 90.25) / 20 = 4.75, +/// sigma = 2.179449... and the upper band is 100.5 + 2 * 2.179449 = 104.858899.... +/// A close of 110 clears it, and on bar 19 the close sat exactly on a zero-width band. +/// +public class BollingerBreakoutStrategyTests +{ + private static IReadOnlyList BreakoutSeries(double breakoutClose) + { + List closes = []; + for (int i = 0; i < 20; i++) + { + closes.Add(100.0); + } + + closes.Add(breakoutClose); + closes.Add(breakoutClose); + + return TestBars.FromCloses(closes, wick: 0.0); + } + + [Fact] + public void ACloseBreakingAboveTheUpperBandEmitsALongSignal() + { + // Arrange + IReadOnlyList bars = BreakoutSeries(110.0); + StrategyDriver driver = new(new BollingerBreakoutStrategy(20, 2.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert - the hand-computed band values first. + BollingerBandSeries bands = driver.Indicators.BollingerBands(20, 2.0, 2.0); + bands.Upper[19].ShouldBe(100.0, 1e-9); + bands.Middle[20].ShouldBe(100.5, 1e-9); + bands.Upper[20].ShouldBe(100.5 + (2.0 * Math.Sqrt(4.75)), 1e-9); + + signals[19].ShouldBe(Signal.Hold); + signals[20].ShouldBe(Signal.EnterLong); + } + + [Fact] + public void ACloseBreakingBelowTheLowerBandEmitsAShortSignal() + { + // Arrange - symmetric fixture: the same move downwards. + IReadOnlyList bars = BreakoutSeries(90.0); + StrategyDriver driver = new(new BollingerBreakoutStrategy(20, 2.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert + BollingerBandSeries bands = driver.Indicators.BollingerBands(20, 2.0, 2.0); + bands.Middle[20].ShouldBe(99.5, 1e-9); + bands.Lower[20].ShouldBe(99.5 - (2.0 * Math.Sqrt(4.75)), 1e-9); + + signals[20].ShouldBe(Signal.EnterShort); + } + + [Fact] + public void ALongIsClosedWhenThePriceFallsBackToTheMiddleBand() + { + // Arrange - break out to 110, then drift back to the moving average. + List closes = []; + for (int i = 0; i < 20; i++) + { + closes.Add(100.0); + } + + closes.Add(110.0); + closes.Add(100.0); + + IReadOnlyList bars = TestBars.FromCloses(closes, wick: 0.0); + Position longPosition = new(OrderSide.Buy, 10.0, 110.0, 21, TestBars.Origin, 0.0); + StrategyDriver driver = new(new BollingerBreakoutStrategy(20, 2.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1, i => i > 20 ? longPosition : null); + + // Assert - on bar 21 the close (100) is at or below the middle band (100.5), so the breakout is spent. + BollingerBandSeries bands = driver.Indicators.BollingerBands(20, 2.0, 2.0); + bands.Middle[21].ShouldBe(100.5, 1e-9); + signals[21].ShouldBe(Signal.Exit); + } + + [Fact] + public void NoSignalIsEmittedWhileTheBandsAreStillWarmingUp() + { + // Arrange + IReadOnlyList bars = BreakoutSeries(110.0); + StrategyDriver driver = new(new BollingerBreakoutStrategy(20, 2.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert - BBANDS(20) has no value before bar 19, and the rule also needs bar 18. + signals.Take(20).ShouldAllBe(signal => signal == Signal.Hold); + } + + [Theory] + [InlineData(1, 2.0)] + [InlineData(20, 0.0)] + [InlineData(20, -1.0)] + public void AnInvalidConfigurationIsRejected(int timePeriod, double deviations) + { + // Arrange / Act / Assert + Should.Throw(() => new BollingerBreakoutStrategy(timePeriod, deviations)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/MacdTrendStrategyTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/MacdTrendStrategyTests.cs new file mode 100644 index 00000000..66a39dee --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/MacdTrendStrategyTests.cs @@ -0,0 +1,163 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Strategies; + +public class MacdTrendStrategyTests +{ + private static readonly Position s_longPosition = new(OrderSide.Buy, 10.0, 100.0, 1, TestBars.Origin, 0.0); + + /// + /// 20 quiet bars around 100 with a one-point range, then a collapse to 50. + /// ATR(14) settles near 1, so a 3x ATR trailing stop sits near 97 and the collapse must breach it. + /// + private static List CollapseSeries() + { + List bars = []; + for (int i = 0; i < 20; i++) + { + bars.Add(new Bar(TestBars.Origin.AddDays(i), 100.0, 100.5, 99.5, 100.0, 1_000.0)); + } + + bars.Add(new Bar(TestBars.Origin.AddDays(20), 100.0, 100.0, 50.0, 50.0, 1_000.0)); + bars.Add(new Bar(TestBars.Origin.AddDays(21), 50.0, 50.5, 49.5, 50.0, 1_000.0)); + + return bars; + } + + [Fact] + public void TheAtrTrailingStopExitsALongOnTheBarThatBreachesIt() + { + // Arrange + List bars = CollapseSeries(); + StrategyDriver driver = new(new MacdTrendStrategy(12, 26, 9, 14, 3.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1, _ => s_longPosition); + + // Assert - the stop is armed and unbreached while the price is quiet ... + IndicatorSeries atr = driver.Indicators.Atr(14); + atr.TryGetValue(19, out double quietAtr).ShouldBeTrue(); + quietAtr.ShouldBeGreaterThan(0.0); + (100.0 - (3.0 * quietAtr)).ShouldBeLessThan(100.0); + signals[19].ShouldBe(Signal.Hold); + + // ... and the collapse to 50 is far below it, so the position is closed on that very bar. + signals[20].ShouldBe(Signal.Exit); + } + + [Fact] + public void TheTrailingStopIsIgnoredWhileTheAccountIsFlat() + { + // Arrange + List bars = CollapseSeries(); + StrategyDriver driver = new(new MacdTrendStrategy(12, 26, 9, 14, 3.0), bars); + + // Act - no position on any bar. + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert - MACD needs 26 + 9 bars, so nothing at all can fire on this 22-bar fixture. + signals.ShouldAllBe(signal => signal == Signal.Hold); + } + + /// + /// 15 quiet bars whose true range is exactly 1, then a 10-point rally, then a pull-back. + /// + /// + /// + /// bar O H L C TR ATR(14) stop = C - 3 x ATR + /// 14 100 100.5 99.5 100 1 1 97 armed here + /// 15 100 110.5 99.5 110 11 24/14 104.857... ratchets up + /// 16 110 110.0 99.5 100 10.5 - - candidate is lower, stop holds + /// + /// On bar 16 the close of 100 is below the ratcheted stop of 104.857 even though it is still at the + /// level the position was opened at, so the trailing stop β€” and only the trailing stop β€” closes it. + /// + private static List RallyThenPullBackSeries() + { + List bars = []; + for (int i = 0; i <= 14; i++) + { + bars.Add(new Bar(TestBars.Origin.AddDays(i), 100.0, 100.5, 99.5, 100.0, 1_000.0)); + } + + bars.Add(new Bar(TestBars.Origin.AddDays(15), 100.0, 110.5, 99.5, 110.0, 1_000.0)); + bars.Add(new Bar(TestBars.Origin.AddDays(16), 110.0, 110.0, 99.5, 100.0, 1_000.0)); + + return bars; + } + + [Fact] + public void TheStopRatchetsUpwardsAndNeverGivesGround() + { + // Arrange + List bars = RallyThenPullBackSeries(); + StrategyDriver driver = new(new MacdTrendStrategy(12, 26, 9, 14, 3.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1, _ => s_longPosition); + + // Assert - the two ATR values the stop is built from, hand-computed above. + IndicatorSeries atr = driver.Indicators.Atr(14); + atr.BegIdx.ShouldBe(14); + atr[14].ShouldBe(1.0, 1e-12); + atr[15].ShouldBe(24.0 / 14.0, 1e-12); + + // The stop is armed at 97 on bar 14 and ratchets up to 104.857 on bar 15; neither bar breaches it. + signals[14].ShouldBe(Signal.Hold); + signals[15].ShouldBe(Signal.Hold); + + // The pull-back to 100 is above the original stop of 97 but below the ratcheted one, so the exit + // can only be explained by the ratchet having moved. + double originalStop = 100.0 - (3.0 * 1.0); + double ratchetedStop = 110.0 - (3.0 * (24.0 / 14.0)); + bars[16].Close.ShouldBeGreaterThan(originalStop); + bars[16].Close.ShouldBeLessThan(ratchetedStop); + signals[16].ShouldBe(Signal.Exit); + } + + [Fact] + public void MacdCrossingsDriveTheEntriesOnALongEnoughSeries() + { + // Arrange - a series with a genuine down leg followed by a genuine up leg produces both crossings. + IReadOnlyList bars = SyntheticSeriesGenerator.Generate(400, seed: 777); + StrategyDriver driver = new(new MacdTrendStrategy(), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert - every entry signal sits exactly on a bar where the MACD line crosses its signal line. + MacdSeries macd = driver.Indicators.Macd(12, 26, 9); + signals.Count(signal => signal is Signal.EnterLong or Signal.EnterShort).ShouldBeGreaterThan(0); + + for (int i = 0; i < signals.Count; i++) + { + if (signals[i] is not (Signal.EnterLong or Signal.EnterShort)) + { + continue; + } + + macd.Line.TryGetPair(i, out double previousLine, out double currentLine).ShouldBeTrue(); + macd.Signal.TryGetPair(i, out double previousSignal, out double currentSignal).ShouldBeTrue(); + + bool crossedUp = previousLine <= previousSignal && currentLine > currentSignal; + bool crossedDown = previousLine >= previousSignal && currentLine < currentSignal; + (crossedUp || crossedDown).ShouldBeTrue($"bar {i} emitted {signals[i]} without a MACD crossing"); + } + } + + [Theory] + [InlineData(0, 26, 9, 14, 3.0)] + [InlineData(26, 12, 9, 14, 3.0)] + [InlineData(12, 26, 0, 14, 3.0)] + [InlineData(12, 26, 9, 0, 3.0)] + [InlineData(12, 26, 9, 14, 0.0)] + public void AnInvalidConfigurationIsRejected(int fast, int slow, int signal, int atrPeriod, double atrMultiple) + { + // Arrange / Act / Assert + Should.Throw(() => new MacdTrendStrategy(fast, slow, signal, atrPeriod, atrMultiple)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/RsiMeanReversionStrategyTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/RsiMeanReversionStrategyTests.cs new file mode 100644 index 00000000..eff9b656 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/RsiMeanReversionStrategyTests.cs @@ -0,0 +1,135 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Strategies; + +/// +/// The fixture is 21 bars of steady one-point decline (which drives RSI(14) to zero) followed by one large +/// up bar, which is enough to lift RSI back above the oversold level in a single step. +/// +public class RsiMeanReversionStrategyTests +{ + private const int ReboundBar = 21; + + private static IReadOnlyList Series() + { + List closes = []; + for (int i = 0; i <= 20; i++) + { + closes.Add(100.0 - i); + } + + closes.Add(110.0); + for (int i = 0; i < 10; i++) + { + closes.Add(110.0 + i); + } + + return TestBars.FromCloses(closes, wick: 0.0); + } + + [Fact] + public void CrossingBackAboveTheOversoldLevelEmitsALongSignal() + { + // Arrange + IReadOnlyList bars = Series(); + StrategyDriver driver = new(new RsiMeanReversionStrategy(14, 30.0, 70.0, 50.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert - the precondition of the rule is verified first, so the test explains itself when it fails. + IndicatorSeries rsi = driver.Indicators.Rsi(14); + rsi.TryGetValue(ReboundBar - 1, out double previous).ShouldBeTrue(); + rsi.TryGetValue(ReboundBar, out double current).ShouldBeTrue(); + previous.ShouldBeLessThanOrEqualTo(30.0); + current.ShouldBeGreaterThan(30.0); + + signals[ReboundBar].ShouldBe(Signal.EnterLong); + } + + [Fact] + public void NoSignalIsEmittedWhileTheIndicatorIsStillWarmingUp() + { + // Arrange - RSI(14) has no value before bar 14, and the rule also needs bar 13. + IReadOnlyList bars = Series(); + StrategyDriver driver = new(new RsiMeanReversionStrategy(), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1); + + // Assert + signals.Take(15).ShouldAllBe(signal => signal == Signal.Hold); + } + + [Fact] + public void ALongPositionIsHeldWhileTheIndicatorStaysBelowTheExitLevel() + { + // Arrange - during the decline RSI sits near zero, far below the exit level of 50. + IReadOnlyList bars = Series(); + Position longPosition = new(OrderSide.Buy, 10.0, 100.0, 15, TestBars.Origin, 0.0); + StrategyDriver driver = new(new RsiMeanReversionStrategy(14, 30.0, 70.0, 50.0), bars); + + // Act - carry the same long position from bar 15 onwards so only the exit rule can fire. + IReadOnlyList signals = driver.StepThrough(bars.Count - 1, i => i >= 15 ? longPosition : null); + + // Assert + IndicatorSeries rsi = driver.Indicators.Rsi(14); + for (int i = 15; i < ReboundBar; i++) + { + rsi[i].ShouldBeLessThan(50.0); + signals[i].ShouldBe(Signal.Hold); + } + } + + [Fact] + public void ALongPositionIsClosedOnTheFirstBarAtOrAboveTheExitLevel() + { + // Arrange + IReadOnlyList bars = Series(); + Position longPosition = new(OrderSide.Buy, 10.0, 100.0, 15, TestBars.Origin, 0.0); + StrategyDriver driver = new(new RsiMeanReversionStrategy(14, 30.0, 70.0, 50.0), bars); + + // Act + IReadOnlyList signals = driver.StepThrough(bars.Count - 1, i => i >= 15 ? longPosition : null); + + // Assert - the rebound bar itself emits the entry (the entry rules are tested first), and the very + // next bar, still above 50, closes the position. The rule is a level test, not a crossing test. + IndicatorSeries rsi = driver.Indicators.Rsi(14); + signals[ReboundBar].ShouldBe(Signal.EnterLong); + rsi[ReboundBar + 1].ShouldBeGreaterThanOrEqualTo(50.0); + signals[ReboundBar + 1].ShouldBe(Signal.Exit); + } + + [Fact] + public void AShortPositionIsNotClosedByTheLongExitRule() + { + // Arrange - RSI is far above 50 late in the rising leg, which must not close a short. + IReadOnlyList bars = Series(); + Position shortPosition = new(OrderSide.Sell, 10.0, 100.0, 0, TestBars.Origin, 0.0); + StrategyDriver driver = new(new RsiMeanReversionStrategy(14, 30.0, 70.0, 50.0), bars); + driver.StepThrough(bars.Count - 2, _ => shortPosition); + + // Act + Signal signal = driver.StepTo(bars.Count - 1, shortPosition); + + // Assert + IndicatorSeries rsi = driver.Indicators.Rsi(14); + rsi[bars.Count - 1].ShouldBeGreaterThan(50.0); + signal.ShouldNotBe(Signal.Exit); + } + + [Theory] + [InlineData(0, 30, 70, 50)] + [InlineData(14, 70, 30, 50)] + [InlineData(14, 30, 70, 20)] + [InlineData(14, 30, 70, 80)] + public void AnInvalidConfigurationIsRejected(int period, double oversold, double overbought, double exit) + { + // Arrange / Act / Assert + Should.Throw(() => new RsiMeanReversionStrategy(period, oversold, overbought, exit)); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/SmaCrossoverStrategyTests.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/SmaCrossoverStrategyTests.cs new file mode 100644 index 00000000..f14c4183 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/Strategies/SmaCrossoverStrategyTests.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.Strategies; + +/// +/// The fixture is chosen so that both moving averages can be computed by hand. +/// +/// +/// Closes: 10 10 10 10 10 20 20 20 20 5 5 5 5 with SMA(2) against SMA(4). +/// +/// bar close SMA2 SMA4 relation +/// 4 10 10 10 equal +/// 5 20 15 12.5 fast above -> golden cross on bar 5 +/// 8 20 20 20 equal +/// 9 5 12.5 16.25 fast below -> death cross on bar 9 +/// +/// +public class SmaCrossoverStrategyTests +{ + private static readonly IReadOnlyList s_series = + TestBars.FromCloses([10, 10, 10, 10, 10, 20, 20, 20, 20, 5, 5, 5, 5], wick: 0.0); + + [Fact] + public void TheGoldenCrossBarEmitsALongSignal() + { + // Arrange + StrategyDriver driver = new(new SmaCrossoverStrategy(2, 4), s_series); + + // Act + IReadOnlyList signals = driver.StepThrough(s_series.Count - 1); + + // Assert - the averages are equal on bar 4 and the fast one is strictly above on bar 5. + IndicatorSeries fast = driver.Indicators.Sma(2); + IndicatorSeries slow = driver.Indicators.Sma(4); + fast[4].ShouldBe(10.0, 1e-12); + slow[4].ShouldBe(10.0, 1e-12); + fast[5].ShouldBe(15.0, 1e-12); + slow[5].ShouldBe(12.5, 1e-12); + + signals[4].ShouldBe(Signal.Hold); + signals[5].ShouldBe(Signal.EnterLong); + signals[6].ShouldBe(Signal.Hold); + } + + [Fact] + public void TheDeathCrossBarEmitsAShortSignal() + { + // Arrange + StrategyDriver driver = new(new SmaCrossoverStrategy(2, 4), s_series); + + // Act + IReadOnlyList signals = driver.StepThrough(s_series.Count - 1); + + // Assert + IndicatorSeries fast = driver.Indicators.Sma(2); + IndicatorSeries slow = driver.Indicators.Sma(4); + fast[9].ShouldBe(12.5, 1e-12); + slow[9].ShouldBe(16.25, 1e-12); + + signals[8].ShouldBe(Signal.Hold); + signals[9].ShouldBe(Signal.EnterShort); + } + + [Fact] + public void NoSignalIsEmittedWhileTheSlowAverageIsStillWarmingUp() + { + // Arrange - SMA(4) has no value before bar 3, and the rule also needs bar 2. + StrategyDriver driver = new(new SmaCrossoverStrategy(2, 4), s_series); + + // Act + IReadOnlyList signals = driver.StepThrough(s_series.Count - 1); + + // Assert + signals.Take(4).ShouldAllBe(signal => signal == Signal.Hold); + } + + [Fact] + public void EvaluateBeforeInitializeIsRejected() + { + // Arrange + SmaCrossoverStrategy strategy = new(2, 4); + BarWindow window = new(s_series, 5); + + // Act / Assert + Should.Throw(() => strategy.Evaluate(window, null)); + } + + [Theory] + [InlineData(0, 10)] + [InlineData(10, 10)] + [InlineData(50, 20)] + public void AnInvalidPeriodPairIsRejected(int fast, int slow) + { + // Arrange / Act / Assert + Should.Throw(() => new SmaCrossoverStrategy(fast, slow)); + } + + [Fact] + public void TheNameAndDescriptionCarryTheConfiguredPeriods() + { + // Arrange + SmaCrossoverStrategy strategy = new(20, 60); + + // Act / Assert + strategy.Name.ShouldBe("SMA 20/60"); + strategy.Description.ShouldContain("SMA20"); + strategy.Description.ShouldContain("SMA60"); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TechnicalAnalysis.Samples.Backtesting.UnitTests.csproj b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TechnicalAnalysis.Samples.Backtesting.UnitTests.csproj new file mode 100644 index 00000000..d6edd325 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TechnicalAnalysis.Samples.Backtesting.UnitTests.csproj @@ -0,0 +1,11 @@ + + + + TechnicalAnalysis.Samples.Backtesting.UnitTests + + + + + + + diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/CheatingStrategies.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/CheatingStrategies.cs new file mode 100644 index 00000000..29cee25f --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/CheatingStrategies.cs @@ -0,0 +1,149 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.TestSupport; + +/// +/// A strategy that reads tomorrow's bar and buys whenever tomorrow closes higher β€” the canonical look-ahead +/// bug. It must be impossible to run. +/// +internal sealed class FutureBarPeekingStrategy : IStrategy +{ + private readonly int _peekOffset; + + internal FutureBarPeekingStrategy(int peekOffset = 1) + { + _peekOffset = peekOffset; + } + + public string Name => "Cheater (reads a future bar)"; + + public string Description => "Reads the close of a bar that has not happened yet."; + + public void Initialize(IIndicatorSource indicators) + { + } + + public Signal Evaluate(IBarWindow bars, Position? position) + { + Bar tomorrow = bars[bars.CurrentIndex + _peekOffset]; + return tomorrow.Close > bars.Current.Close ? Signal.EnterLong : Signal.Exit; + } +} + +/// +/// A strategy that reads a future value of an indicator rather than of a bar. An indicator computed from +/// tomorrow's close is exactly as fatal as tomorrow's close itself, so this must fail too. +/// +internal sealed class FutureIndicatorPeekingStrategy : IStrategy +{ + private IndicatorSeries? _sma; + + public string Name => "Cheater (reads a future indicator value)"; + + public string Description => "Reads an indicator value belonging to a bar that has not happened yet."; + + public void Initialize(IIndicatorSource indicators) + { + _sma = indicators.Sma(3); + } + + public Signal Evaluate(IBarWindow bars, Position? position) + { + return _sma!.TryGetValue(bars.CurrentIndex + 1, out double future) && future > bars.Current.Close + ? Signal.EnterLong + : Signal.Hold; + } +} + +/// +/// A strategy that tries to discover how long the series is by walking forward until something stops it. +/// The window must stop it on the very first step past the current bar. +/// +internal sealed class SeriesLengthProbingStrategy : IStrategy +{ + public string Name => "Cheater (probes the series length)"; + + public string Description => "Walks forward past the current bar to discover the length of the series."; + + public void Initialize(IIndicatorSource indicators) + { + } + + public Signal Evaluate(IBarWindow bars, Position? position) + { + int probe = bars.CurrentIndex; + while (true) + { + probe++; + _ = bars[probe]; + } + } +} + +/// +/// A strategy that tries to learn the total length of the series from an rather +/// than from the bar window β€” the same end-of-sample cheat, taken through the back door. The indicator is +/// obtained in , before bar 0 is ever evaluated, which is exactly when knowing the +/// end date would be most valuable and most impossible in live trading. +/// +internal sealed class IndicatorLengthProbingStrategy : IStrategy +{ + private IndicatorSeries? _sma; + + public string Name => "Cheater (probes an indicator for the series length)"; + + public string Description => "Reads IndicatorSeries.Count to discover how many bars exist in total."; + + /// + /// Gets the value Count reported in , before any bar was evaluated. + /// + internal int CountAtInitialize { get; private set; } = -1; + + /// + /// Gets the Count, BegIdx and NBElement observed on each bar, in call order. + /// + internal List<(int Count, int BegIdx, int NBElement)> Observed { get; } = []; + + public void Initialize(IIndicatorSource indicators) + { + Observed.Clear(); + _sma = indicators.Sma(3); + CountAtInitialize = _sma.Count; + } + + public Signal Evaluate(IBarWindow bars, Position? position) + { + Observed.Add((_sma!.Count, _sma.BegIdx, _sma.NBElement)); + return Signal.Hold; + } +} + +/// +/// A strategy that binary-searches for the end of the series, using +/// the difference between "returned false" and "threw" as an oracle. Every forward index must throw, so the +/// oracle must not exist. +/// +internal sealed class IndicatorPairProbingStrategy : IStrategy +{ + private IndicatorSeries? _sma; + + public string Name => "Cheater (probes TryGetPair for the series length)"; + + public string Description => "Calls TryGetPair far past the current bar to find where the series ends."; + + public void Initialize(IIndicatorSource indicators) + { + _sma = indicators.Sma(3); + } + + public Signal Evaluate(IBarWindow bars, Position? position) + { + // A quiet false here would say "past the end of the series"; a throw says "in the future". Only the + // second answer is allowed, whatever the probe index. + return _sma!.TryGetPair(bars.CurrentIndex + 1_000_000, out _, out _) ? Signal.EnterLong : Signal.Hold; + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/ScriptedStrategy.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/ScriptedStrategy.cs new file mode 100644 index 00000000..8b47fff3 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/ScriptedStrategy.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.TestSupport; + +/// +/// A strategy whose signal on each bar is dictated by the test, so the engine's execution and accounting can +/// be checked against arithmetic done by hand. +/// +internal sealed class ScriptedStrategy : IStrategy +{ + private readonly Func _script; + + internal ScriptedStrategy(Func script, string name = "Scripted") + { + _script = script; + Name = name; + } + + /// + /// Creates a strategy that emits a specific signal on specific bar indices and holds everywhere else. + /// + internal static ScriptedStrategy At(IReadOnlyDictionary signals, string name = "Scripted") + { + return new ScriptedStrategy( + (bars, _) => signals.TryGetValue(bars.CurrentIndex, out Signal signal) ? signal : Signal.Hold, + name); + } + + public string Name { get; } + + public string Description => "Test double whose signals are supplied by the test."; + + /// + /// Gets the bar indices was called on, in call order. + /// + internal List ObservedIndices { get; } = []; + + /// + /// Gets the number of visible bars reported by the window on each call, in call order. + /// + internal List ObservedCounts { get; } = []; + + public void Initialize(IIndicatorSource indicators) + { + ObservedIndices.Clear(); + ObservedCounts.Clear(); + } + + public Signal Evaluate(IBarWindow bars, Position? position) + { + ObservedIndices.Add(bars.CurrentIndex); + ObservedCounts.Add(bars.Count); + return _script(bars, position); + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/StrategyDriver.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/StrategyDriver.cs new file mode 100644 index 00000000..f419699d --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/StrategyDriver.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.TestSupport; + +/// +/// Drives a strategy bar by bar exactly the way does β€” same cursor, same +/// indicator binding β€” but without the accounting, so a test can assert on the raw signal of a chosen bar. +/// +internal sealed class StrategyDriver +{ + private readonly IStrategy _strategy; + private readonly BarWindow _window; + + internal StrategyDriver(IStrategy strategy, IReadOnlyList bars) + { + _strategy = strategy; + _window = new BarWindow(bars); + Indicators = new IndicatorSet(bars, _window); + _strategy.Initialize(Indicators); + } + + internal IndicatorSet Indicators { get; } + + internal IBarWindow Window => _window; + + /// + /// Moves the cursor to and returns the signal the strategy emits there. + /// + internal Signal StepTo(int barIndex, Position? position = null) + { + _window.MoveTo(barIndex); + return _strategy.Evaluate(_window, position); + } + + /// + /// Steps through bars 0..lastIndex in order, feeding the same fixed position on every bar, and + /// returns the signal emitted on each one. + /// + internal IReadOnlyList StepThrough(int lastIndex, Func? positionAt = null) + { + List signals = new(lastIndex + 1); + for (int i = 0; i <= lastIndex; i++) + { + signals.Add(StepTo(i, positionAt?.Invoke(i))); + } + + return signals; + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/TestBars.cs b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/TestBars.cs new file mode 100644 index 00000000..1b431584 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/TestSupport/TestBars.cs @@ -0,0 +1,69 @@ +// Copyright (c) 2023 Philippe Matray. All rights reserved. +// This file is part of TaLibStandard. +// TaLibStandard is licensed under the GNU General Public License v3.0. +// See the LICENSE file in the project root for the full license text. +// For more information, visit https://github.com/phmatray/TaLibStandard. + +namespace TechnicalAnalysis.Samples.Backtesting.UnitTests.TestSupport; + +/// +/// Builders for the small, fully hand-controlled bar series the tests reason about. +/// +internal static class TestBars +{ + internal static readonly DateTime Origin = new(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Builds bars from a close series. Each bar opens at the previous close (the first at its own close), + /// and the high and low are widened by so that every bar is well formed. + /// + internal static IReadOnlyList FromCloses(IReadOnlyList closes, double wick = 0.5) + { + List bars = new(closes.Count); + for (int i = 0; i < closes.Count; i++) + { + double open = i == 0 ? closes[0] : closes[i - 1]; + double close = closes[i]; + double high = Math.Max(open, close) + wick; + double low = Math.Min(open, close) - wick; + bars.Add(new Bar(Origin.AddDays(i), open, high, low, close, 1_000.0)); + } + + return bars; + } + + /// + /// Builds bars from explicit open/close pairs, keeping the wick tight around the body. + /// + internal static IReadOnlyList FromOpenClose(IReadOnlyList<(double Open, double Close)> prices) + { + List bars = new(prices.Count); + for (int i = 0; i < prices.Count; i++) + { + (double open, double close) = prices[i]; + bars.Add(new Bar( + Origin.AddDays(i), + open, + Math.Max(open, close), + Math.Min(open, close), + close, + 1_000.0)); + } + + return bars; + } + + /// + /// Builds a perfectly flat series: every price of every bar is . + /// + internal static IReadOnlyList Flat(int count, double price = 100.0) + { + List bars = new(count); + for (int i = 0; i < count; i++) + { + bars.Add(new Bar(Origin.AddDays(i), price, price, price, price, 1_000.0)); + } + + return bars; + } +} diff --git a/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/xunit.runner.json b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/xunit.runner.json new file mode 100644 index 00000000..86c7ea05 --- /dev/null +++ b/tests/TechnicalAnalysis.Samples.Backtesting.UnitTests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/tools/generate-indicator-catalog.py b/tools/generate-indicator-catalog.py new file mode 100755 index 00000000..e0328713 --- /dev/null +++ b/tools/generate-indicator-catalog.py @@ -0,0 +1,946 @@ +#!/usr/bin/env python3 +"""Generate docs/indicators/README.md, the complete TaLibStandard indicator catalog. + +WHAT IT DOES + Parses every ``src/TechnicalAnalysis.Functions/*/TAMath.cs`` and + ``src/TechnicalAnalysis.Candles/*/TACandle.cs`` file, extracts every ``public static`` + method (name, generic arity, full parameter list including default values, return type + and the XML ```` text), resolves the named output properties of each + ``*Result`` record, groups the entry points into the canonical TA-Lib categories using + the CATEGORIES table below, verifies that the DefaultDocumentation page for each entry + point really exists under ``docs/functions`` / ``docs/candles``, and writes + ``docs/indicators/README.md``. + +HOW TO RE-RUN IT + From anywhere (the script locates the repository root relative to its own path): + + python3 tools/generate-indicator-catalog.py + + Useful flags: + --check do not write; exit 1 if docs/indicators/README.md is out of date + --output P write to P instead of docs/indicators/README.md + --quiet suppress the progress report on stdout + + Requires python3 only - no third-party imports. + +WHEN IT FAILS + The script exits non-zero and names the offending entry points when a discovered + indicator is missing from the CATEGORIES mapping table, or when the same indicator is + listed in two categories, or when a category references an indicator that no longer + exists in the source tree. That is deliberate: adding a new indicator to the library + forces a conscious edit here, so the catalog can never silently go stale. +""" + +from __future__ import annotations + +import argparse +import os +import re +import sys +from dataclasses import dataclass, field + +# --------------------------------------------------------------------------- +# Category mapping table - the single source of truth for how entry points are +# grouped. Names are the C# method names on TAMath / TACandle, NOT the TA-Lib C +# names. Keep the categories in canonical TA-Lib order. +# --------------------------------------------------------------------------- + +# --------------------------------------------------------------------------- +# Known defects. Entry points whose output is currently wrong, keyed by the C# +# method name on TAMath. Each row of the catalog for one of these names carries +# a marker pointing at the "Known defects" section, so a reader who arrives at a +# single row from a search engine still sees the warning. +# +# The values are the short form shown in the section's table. Keep them factual +# and measured; remove an entry the moment the underlying defect is fixed. +# --------------------------------------------------------------------------- + +DEFECT_ATR = "atr-diverges" +DEFECT_EMA = "ema-seed" +DEFECT_RSI = "rsi-nan" + +KNOWN_DEFECTS: dict[str, str] = { + "Atr": DEFECT_ATR, + "Ema": DEFECT_EMA, + "Macd": DEFECT_EMA, + "MacdExt": DEFECT_EMA, + "MacdFix": DEFECT_EMA, + "Dema": DEFECT_EMA, + "Tema": DEFECT_EMA, + "T3": DEFECT_EMA, + "Apo": DEFECT_EMA, + "Ppo": DEFECT_EMA, + "Trix": DEFECT_EMA, + "Rsi": DEFECT_RSI, +} + +DEFECT_ROWS: list[tuple[str, str, str, str]] = [ + ( + DEFECT_ATR, + "`Atr` never divides its running average", + "`Atr`", + "The main output loop divides the emitted value by `period` but never the accumulator, so the running " + "average is multiplied by `period - 1` on every bar. Measured on a series whose true range is exactly " + "`2.0` every bar, `Atr(..., 14)` returns `2, 2, 26.142857, 340, 4420.142857, 57462, 747006.142857, " + "9711080, ...`; on a 1500-bar series it reaches `+inf` by bar 300. Only the first two outputs are " + "usable. `Natr` is **not** affected. Workaround: Wilder-smooth `TrueRange` yourself. " + "Source: `src/TechnicalAnalysis.Functions/Atr/TAFunc.cs`", + ), + ( + DEFECT_EMA, + "`TA_INT_EMA` seeds itself low", + "`Ema`, `Macd`, `MacdExt`, `MacdFix`, `Dema`, `Tema`, `T3`, `Apo`, `Ppo`, `Trix`", + "The seed loop sums `period - 1` inputs and divides by `period` (upstream TA-Lib sums `period` of " + "them), and the loop that follows applies one extra smoothing step. `Ema` over a constant series of " + "`100` with `timePeriod: 20` returns `95.476190` instead of `100`. The error decays with the smoothing " + "factor, so it distorts the bars just after warm-up rather than the steady state; raising " + "`TACore.Globals.UnstablePeriod[FuncUnstId.Ema]` discards them. " + "Source: `src/TechnicalAnalysis.Functions/TAFunc.cs`, `TA_INT_EMA`", + ), + ( + DEFECT_RSI, + "`Rsi` returns `NaN` on a perfectly flat series", + "`Rsi`", + "There is no zero guard on `prevGain + prevLoss`, so a window with no price change divides by zero. " + "The call still reports `RetCode.Success` with a non-zero `NBElement`, and every element is `NaN`. " + "Trigger: a halted instrument, or a synthetic constant series. Guard consumers with " + "`double.IsFinite`. Source: `src/TechnicalAnalysis.Functions/Rsi/TAFunc.cs`", + ), +] + +CATEGORIES: list[tuple[str, str, list[str]]] = [ + ( + "Overlap Studies", + "Trend-following overlays plotted on the same scale as price.", + [ + "BollingerBands", # TA-Lib BBANDS + "Dema", + "Ema", + "HtTrendline", # TA-Lib HT_TRENDLINE + "Kama", + "Mama", + "MovingAverage", # TA-Lib MA + "MovingAverageVariablePeriod", # TA-Lib MAVP + "MidPoint", + "MidPrice", + "Sar", + "SarExt", + "Sma", + "T3", + "Tema", + "Trima", + "Wma", + "ZigZag", # TaLibStandard extension, no TA-Lib C equivalent + ], + ), + ( + "Momentum Indicators", + "Oscillators and directional-movement measures derived from rate of change.", + [ + "Adx", + "Adxr", + "Apo", + "Aroon", + "AroonOsc", + "Bop", + "Cci", + "Cmo", + "Dx", + "Macd", + "MacdExt", + "MacdFix", + "Mfi", + "MinusDI", # TA-Lib MINUS_DI + "MinusDM", # TA-Lib MINUS_DM + "Mom", + "PlusDI", # TA-Lib PLUS_DI + "PlusDM", # TA-Lib PLUS_DM + "Ppo", + "Roc", + "RocP", + "RocR", + "RocR100", + "Rsi", + "Stoch", + "StochF", + "StochRsi", + "Trix", + "UltOsc", + "WillR", + ], + ), + ( + "Volume Indicators", + "Indicators that combine price with traded volume.", + [ + "Ad", # TA-Lib AD (Chaikin A/D Line) + "AdOsc", # TA-Lib ADOSC (Chaikin A/D Oscillator) + "Obv", + ], + ), + ( + "Volatility Indicators", + "True-range based measures of dispersion.", + [ + "Atr", + "Natr", + "TrueRange", # TA-Lib TRANGE + ], + ), + ( + "Price Transform", + "Single-bar arithmetic combinations of open/high/low/close.", + [ + "AvgPrice", + "MedPrice", + "TypPrice", + "WclPrice", + ], + ), + ( + "Cycle Indicators", + "Hilbert Transform cycle analysis (Ehlers).", + [ + "HtDcPeriod", # TA-Lib HT_DCPERIOD + "HtDcPhase", # TA-Lib HT_DCPHASE + "HtPhasor", # TA-Lib HT_PHASOR + "HtSine", # TA-Lib HT_SINE + "HtTrendMode", # TA-Lib HT_TRENDMODE + ], + ), + ( + "Pattern Recognition", + "Candlestick pattern detection. Every entry point is generic over `T : IFloatingPoint`.", + [], # populated automatically from src/TechnicalAnalysis.Candles - see CANDLE_CATEGORY + ), + ( + "Statistic Functions", + "Regression and dispersion statistics over a rolling window.", + [ + "Beta", + "Correl", + "LinearReg", + "LinearRegAngle", # TA-Lib LINEARREG_ANGLE + "LinearRegIntercept", # TA-Lib LINEARREG_INTERCEPT + "LinearRegSlope", # TA-Lib LINEARREG_SLOPE + "StdDev", + "Tsf", + "Variance", # TA-Lib VAR + ], + ), + ( + "Math Transform", + "Element-wise transcendental and rounding functions.", + [ + "Acos", + "Asin", + "Atan", + "Ceil", + "Cos", + "Cosh", + "Exp", + "Floor", + "Ln", + "Log10", + "Sin", + "Sinh", + "Sqrt", + "Tan", + "Tanh", + ], + ), + ( + "Math Operators", + "Element-wise arithmetic and rolling min/max helpers.", + [ + "Add", + "Div", + "Max", + "MaxIndex", + "Min", + "MinIndex", + "MinMax", + "MinMaxIndex", + "Mult", + "Sub", + "Sum", + ], + ), +] + +# Every entry point discovered in src/TechnicalAnalysis.Candles belongs here. +CANDLE_CATEGORY = "Pattern Recognition" + +# Parameters that describe the calculation range or the raw price/volume inputs. They are +# listed in the Signature column but excluded from the Parameters column, which is meant to +# show only the knobs a caller actually tunes. +INPUT_PARAMETER_NAMES = frozenset( + { + "startIdx", + "endIdx", + "real", + "real0", + "real1", + "open", + "high", + "low", + "close", + "volume", + "periods", + } +) + +# --------------------------------------------------------------------------- +# Source model +# --------------------------------------------------------------------------- + + +@dataclass +class Parameter: + """A single C# parameter of a public static entry point.""" + + type_name: str + name: str + default: str | None + + def declaration(self) -> str: + text = f"{self.type_name} {self.name}" + if self.default is not None: + text += f" = {self.default}" + return text + + +@dataclass +class Overload: + """One `public static` method declaration.""" + + name: str + generic_arity: int + return_type: str + parameters: list[Parameter] + summary: str + + @property + def scalar_kind(self) -> str: + """`double`, `float`, `generic` or `none`, based on the first array parameter.""" + for parameter in self.parameters: + if parameter.type_name == "double[]": + return "double" + if parameter.type_name == "float[]": + return "float" + if parameter.type_name == "T[]": + return "generic" + return "none" + + def declaration(self, holder: str = "") -> str: + params = ", ".join(parameter.declaration() for parameter in self.parameters) + generics = "" if self.generic_arity else "" + qualifier = f"{holder}." if holder else "" + return f"{self.return_type} {qualifier}{self.name}{generics}({params})" + + def parameter_type_list(self) -> str: + return ",".join(parameter.type_name for parameter in self.parameters) + + +@dataclass +class EntryPoint: + """All overloads that share one public method name.""" + + name: str + holder: str # "TAMath" or "TACandle" + source_folder: str + overloads: list[Overload] = field(default_factory=list) + outputs: list[str] = field(default_factory=list) + summary_override: str = "" + + @property + def primary(self) -> Overload: + """The widest non-float overload - the one the docs describe.""" + candidates = [o for o in self.overloads if o.scalar_kind != "float"] + if not candidates: + candidates = list(self.overloads) + return max(candidates, key=lambda o: len(o.parameters)) + + @property + def secondary(self) -> Overload | None: + """A narrower non-float convenience overload, when one exists.""" + candidates = [ + o + for o in self.overloads + if o.scalar_kind != "float" and len(o.parameters) < len(self.primary.parameters) + ] + if not candidates: + return None + return max(candidates, key=lambda o: len(o.parameters)) + + @property + def has_float_overload(self) -> bool: + return any(o.scalar_kind == "float" for o in self.overloads) + + @property + def description(self) -> str: + return self.primary.summary or self.summary_override + + +# --------------------------------------------------------------------------- +# Parsing +# --------------------------------------------------------------------------- + +METHOD_RE = re.compile( + r"^[ \t]*public\s+static\s+" + r"(?P[A-Za-z0-9_]+)\s+" + r"(?P[A-Za-z0-9_]+)" + r"(?P<[^()]*?>)?\s*" + r"\(", + re.MULTILINE, +) + +SUMMARY_RE = re.compile(r"(.*?)", re.DOTALL) +SEE_CREF_RE = re.compile(r"") +XML_TAG_RE = re.compile(r"<[^>]+>") + + +def read_source(path: str) -> str: + """Read a C# file, tolerating the UTF-8 BOM some files carry.""" + with open(path, encoding="utf-8-sig") as handle: + return handle.read() + + +def collapse(text: str) -> str: + return " ".join(text.split()) + + +def clean_summary(raw: str) -> str: + """Turn a raw XML doc body into a single line of plain prose.""" + text = "\n".join(line.strip().removeprefix("///").strip() for line in raw.splitlines()) + text = SEE_CREF_RE.sub(lambda m: m.group(1).split(".")[-1].replace("`1", ""), text) + text = XML_TAG_RE.sub("", text) + text = text.replace("<", "<").replace(">", ">").replace("&", "&") + return collapse(text) + + +def preceding_summary(text: str, method_start: int) -> str: + """Collect the /// block immediately above `method_start` and return its .""" + lines = text[:method_start].splitlines() + doc_lines: list[str] = [] + for line in reversed(lines): + stripped = line.strip() + if stripped.startswith("///"): + doc_lines.append(stripped) + continue + if not stripped: + continue + break + doc_lines.reverse() + match = SUMMARY_RE.search("\n".join(doc_lines)) + return clean_summary(match.group(1)) if match else "" + + +def match_parenthesis(text: str, open_index: int) -> int: + """Return the index of the ')' closing the '(' at `open_index`.""" + depth = 0 + index = open_index + while index < len(text): + char = text[index] + if char == "(": + depth += 1 + elif char == ")": + depth -= 1 + if depth == 0: + return index + index += 1 + raise ValueError(f"unbalanced parentheses starting at offset {open_index}") + + +def split_parameters(raw: str) -> list[str]: + """Split a parameter list on top-level commas (generic arguments may contain commas).""" + parts: list[str] = [] + depth = 0 + current: list[str] = [] + for char in raw: + if char in "<([": + depth += 1 + elif char in ">)]": + depth -= 1 + if char == "," and depth == 0: + parts.append("".join(current)) + current = [] + continue + current.append(char) + if "".join(current).strip(): + parts.append("".join(current)) + return [collapse(part) for part in parts if collapse(part)] + + +def parse_parameter(raw: str) -> Parameter: + default: str | None = None + if "=" in raw: + raw, _, default_text = raw.partition("=") + raw = raw.strip() + default = collapse(default_text) + tokens = collapse(raw).split(" ") + name = tokens[-1] + type_name = " ".join(tokens[:-1]) + return Parameter(type_name=type_name, name=name, default=default) + + +def parse_overloads(path: str) -> list[Overload]: + text = read_source(path) + overloads: list[Overload] = [] + for match in METHOD_RE.finditer(text): + open_index = match.end() - 1 + close_index = match_parenthesis(text, open_index) + raw_params = text[open_index + 1 : close_index] + generics = match.group("generics") or "" + overloads.append( + Overload( + name=match.group("name"), + generic_arity=generics.count(",") + 1 if generics else 0, + return_type=match.group("return"), + parameters=[parse_parameter(part) for part in split_parameters(raw_params)], + summary=preceding_summary(text, match.start()), + ) + ) + return overloads + + +RECORD_BASE_RE = re.compile(r"public\s+record\s+[A-Za-z0-9_]+\s*:\s*([A-Za-z0-9_]+)") +EXPRESSION_PROPERTY_RE = re.compile( + r"public\s+(?:double|int)\[\]\s+([A-Za-z0-9_]+)\s*=>\s*Real([0-9])\s*;" +) +AUTO_PROPERTY_RE = re.compile(r"public\s+(?:double|int)\[\]\s+([A-Za-z0-9_]+)\s*\{\s*get;") + +IMPLICIT_OUTPUTS = { + "SingleOutputResult": ["Real"], + "CandleIndicatorResult": ["Integers"], +} + + +def parse_outputs(result_path: str) -> list[str]: + """Return the public array properties a *Result record exposes, in output-slot order.""" + text = read_source(result_path) + slotted = sorted( + ((int(slot), name) for name, slot in EXPRESSION_PROPERTY_RE.findall(text)), + ) + outputs = [name for _, name in slotted] + for name in AUTO_PROPERTY_RE.findall(text): + if name not in outputs: + outputs.append(name) + if outputs: + return outputs + base_match = RECORD_BASE_RE.search(text) + base = base_match.group(1) if base_match else "" + return list(IMPLICIT_OUTPUTS.get(base, [])) + + +def parse_class_summary(path: str) -> str: + """Return the of the first public class/record declared in `path`.""" + text = read_source(path) + match = SUMMARY_RE.search(text) + return clean_summary(match.group(1)) if match else "" + + +# --------------------------------------------------------------------------- +# Discovery +# --------------------------------------------------------------------------- + + +def indicator_folders(container: str) -> list[str]: + return sorted( + entry + for entry in os.listdir(container) + if os.path.isdir(os.path.join(container, entry)) and entry not in {"bin", "obj", "Internal"} + ) + + +def discover_functions(repo_root: str) -> list[EntryPoint]: + container = os.path.join(repo_root, "src", "TechnicalAnalysis.Functions") + entry_points: list[EntryPoint] = [] + for folder in indicator_folders(container): + math_path = os.path.join(container, folder, "TAMath.cs") + if not os.path.isfile(math_path): + continue + overloads = parse_overloads(math_path) + if not overloads: + continue + names = {overload.name for overload in overloads} + if len(names) != 1: + raise SystemExit(f"{math_path}: expected one method name, found {sorted(names)}") + entry = EntryPoint(name=names.pop(), holder="TAMath", source_folder=folder, overloads=overloads) + result_path = os.path.join(container, folder, f"{entry.primary.return_type}.cs") + if os.path.isfile(result_path): + entry.outputs = parse_outputs(result_path) + entry_points.append(entry) + return entry_points + + +def discover_candles(repo_root: str) -> list[EntryPoint]: + container = os.path.join(repo_root, "src", "TechnicalAnalysis.Candles") + entry_points: list[EntryPoint] = [] + for folder in indicator_folders(container): + candle_path = os.path.join(container, folder, "TACandle.cs") + if not os.path.isfile(candle_path): + continue + overloads = parse_overloads(candle_path) + if not overloads: + continue + names = {overload.name for overload in overloads} + if len(names) != 1: + raise SystemExit(f"{candle_path}: expected one method name, found {sorted(names)}") + entry = EntryPoint(name=names.pop(), holder="TACandle", source_folder=folder, overloads=overloads) + entry.outputs = ["Integers"] + class_path = os.path.join(container, folder, f"{folder}.cs") + if os.path.isfile(class_path): + entry.summary_override = parse_class_summary(class_path) + entry_points.append(entry) + return entry_points + + +# --------------------------------------------------------------------------- +# Documentation links +# --------------------------------------------------------------------------- + + +def url_encode(path: str) -> str: + """Percent-encode the characters that break inline Markdown link targets.""" + return path.replace("(", "%28").replace(")", "%29").replace(" ", "%20") + + +def resolve_doc_link(repo_root: str, entry: EntryPoint) -> tuple[str, str] | None: + """Return (relative_link, label) for the generated page, or None when there is none. + + DefaultDocumentation names a page `Holder.Method.md` when the method has a single + overload group and `Holder.Method_T_(paramTypes).md` when it must disambiguate. + Both shapes are probed and the file must exist on disk before a link is emitted. + """ + folder = "functions" if entry.holder == "TAMath" else "candles" + docs_dir = os.path.join(repo_root, "docs", folder) + generics = "_T_" if entry.primary.generic_arity else "" + candidates = [ + f"{entry.holder}.{entry.name}.md", + f"{entry.holder}.{entry.name}{generics}({entry.primary.parameter_type_list()}).md", + ] + for candidate in candidates: + if os.path.isfile(os.path.join(docs_dir, candidate)): + return f"../{folder}/{url_encode(candidate)}", f"{entry.holder}.{entry.name}" + return None + + +# --------------------------------------------------------------------------- +# Validation +# --------------------------------------------------------------------------- + + +def validate_mapping(entry_points: list[EntryPoint]) -> dict[str, list[EntryPoint]]: + """Bucket entry points by category, exiting non-zero on any mapping mismatch.""" + by_name = {entry.name: entry for entry in entry_points} + + mapped: dict[str, str] = {} + duplicates: list[str] = [] + for category, _, names in CATEGORIES: + for name in names: + if name in mapped: + duplicates.append(f"{name} (in '{mapped[name]}' and '{category}')") + mapped[name] = category + for entry in entry_points: + if entry.holder == "TACandle": + if entry.name in mapped: + duplicates.append(f"{entry.name} (candlestick entry point also listed in '{mapped[entry.name]}')") + mapped[entry.name] = CANDLE_CATEGORY + + unmapped = sorted(name for name in by_name if name not in mapped) + unknown = sorted(name for name in mapped if name not in by_name) + + problems: list[str] = [] + if unmapped: + problems.append( + "The following indicators were discovered in the source tree but are missing from " + f"the CATEGORIES table in {os.path.basename(__file__)}:\n - " + "\n - ".join(unmapped) + ) + if unknown: + problems.append( + "The following indicators are listed in the CATEGORIES table but no longer exist in " + "the source tree:\n - " + "\n - ".join(unknown) + ) + if duplicates: + problems.append("The following indicators are mapped to more than one category:\n - " + "\n - ".join(sorted(set(duplicates)))) + if problems: + sys.stderr.write("ERROR: indicator catalog mapping is out of date.\n\n") + sys.stderr.write("\n\n".join(problems)) + sys.stderr.write("\n\nFix the CATEGORIES table, then re-run this script.\n") + raise SystemExit(1) + + buckets: dict[str, list[EntryPoint]] = {category: [] for category, _, _ in CATEGORIES} + for entry in sorted(entry_points, key=lambda e: e.name): + buckets[mapped[entry.name]].append(entry) + return buckets + + +# --------------------------------------------------------------------------- +# Rendering +# --------------------------------------------------------------------------- + + +def escape_cell(text: str) -> str: + return text.replace("|", "\\|") + + +def code(text: str) -> str: + return f"`{escape_cell(text)}`" if text else "" + + +def tuning_parameters(overload: Overload) -> str: + tunables = [p for p in overload.parameters if p.name not in INPUT_PARAMETER_NAMES] + if not tunables: + return "_none_" + return "
".join(code(p.declaration()) for p in tunables) + + +def describe(entry: EntryPoint) -> str: + """Render the Description cell, prefixed with a defect marker when one applies.""" + text = escape_cell(entry.description) or "_no summary in source_" + if entry.name not in KNOWN_DEFECTS: + return text + + return f"[**KNOWN DEFECT**](#known-defects) - {text}" + + +def signature_cell(entry: EntryPoint) -> str: + lines = [code(entry.primary.declaration(entry.holder))] + secondary = entry.secondary + if secondary is not None: + lines.append(code(secondary.declaration(entry.holder))) + return "
".join(lines) + + +def render(repo_root: str, entry_points: list[EntryPoint], buckets: dict[str, list[EntryPoint]]) -> tuple[str, list[str]]: + functions = [e for e in entry_points if e.holder == "TAMath"] + candles = [e for e in entry_points if e.holder == "TACandle"] + function_overloads = sum(len(e.overloads) for e in functions) + candle_overloads = sum(len(e.overloads) for e in candles) + float_overloads = sum(1 for e in entry_points for o in e.overloads if o.scalar_kind == "float") + + missing_links: list[str] = [] + out: list[str] = [] + add = out.append + + add("# TaLibStandard Indicator Reference") + add("") + add( + "Complete, machine-generated catalog of every public entry point in TaLibStandard. " + "**Do not edit this file by hand** - regenerate it with:" + ) + add("") + add("```bash") + add("python3 tools/generate-indicator-catalog.py") + add("```") + add("") + add( + "The generator parses `src/TechnicalAnalysis.Functions/*/TAMath.cs` and " + "`src/TechnicalAnalysis.Candles/*/TACandle.cs` directly, so this page cannot drift from the " + "source. It fails loudly if a newly added indicator is not classified in its mapping table." + ) + add("") + + add("## What is counted here") + add("") + add(f"| Surface | Entry points | Public `static` overloads |") + add("| --- | ---: | ---: |") + add(f"| `TAMath` (technical indicators, `TechnicalAnalysis.Functions`) | {len(functions)} | {function_overloads} |") + add(f"| `TACandle` (candlestick patterns, `TechnicalAnalysis.Candles`) | {len(candles)} | {candle_overloads} |") + add(f"| **Total** | **{len(entry_points)}** | **{function_overloads + candle_overloads}** |") + add("") + add( + f"An *entry point* is a distinct public method name. There are **{len(functions)} indicator entry points** " + f"and **{len(candles)} candlestick pattern entry points**, i.e. **{len(entry_points)} distinct methods**. " + f"Counting every callable `public static` overload (the `double[]` and `float[]` pairs, plus the " + f"default-argument convenience overloads) gives **{function_overloads + candle_overloads}** methods, " + f"of which **{float_overloads}** are `float[]` overloads. When you see \"200+ indicators\" advertised for " + "TA-Lib ports, that figure is the overload count, not the entry-point count - the honest numbers for " + f"TaLibStandard are {len(functions)} / {len(candles)} / {function_overloads + candle_overloads}." + ) + add("") + + add("### Per category") + add("") + add("| Category | Entry points |") + add("| --- | ---: |") + for category, _, _ in CATEGORIES: + add(f"| [{category}](#{slug(category)}) | {len(buckets[category])} |") + add(f"| **Total** | **{len(entry_points)}** |") + add("") + + add("### Overloads, precision and generic math") + add("") + add( + "- **`double[]` and `float[]` overloads.** Almost every `TAMath` method is declared twice: once " + "taking `double[]` inputs and once taking `float[]`. The `float[]` overload widens its inputs to " + "`double[]` and calls the `double[]` implementation, so it costs one extra allocation and copy per " + "input array and returns exactly the same `*Result` type. All computation is performed in `double`, " + "and every output array is `double[]` (or `int[]`) regardless of the input type." + ) + add( + "- **Convenience overloads.** A handful of entry points expose a shorter overload that hard-codes the " + "TA-Lib default instead of declaring an optional parameter. Both shapes are listed in the *Signature* " + "column below." + ) + add( + "- **Generic-math candlesticks.** `TACandle` methods are generic: " + "`TACandle.CdlDoji(int startIdx, int endIdx, T[] open, T[] high, T[] low, T[] close)` where " + "`T : IFloatingPoint`. That constraint is satisfied by `double`, `float`, `decimal`, `Half` and " + "`System.Runtime.InteropServices.NFloat`, so candlestick detection runs directly on `decimal[]` price " + "arrays with no conversion. All candlestick entry points return " + "`CandleIndicatorResult`, whose `int[] Integers` array holds `0` (no pattern), `+100` (bullish) or " + "`-100` (bearish)." + ) + add("") + + add("### Reading the result objects") + add("") + add( + "Every result record derives from `TechnicalAnalysis.Common.IndicatorResult` and carries three " + "metadata members: `RetCode RetCode`, `int BegIdx` and `int NBElement` (note the capital `B`). " + "**Output element `k` corresponds to input bar `BegIdx + k`, for `k` in `[0, NBElement)`.** Elements " + "at index `>= NBElement` are meaningless zeros. See " + "[the getting-started guide](../guides/getting-started.md) for a fully worked alignment example." + ) + add("") + add( + "The *Outputs* column names the properties that expose the result arrays. `Real` comes from " + "`SingleOutputResult`; multi-output records expose named properties (`RealUpperBand`, `SlowK`, " + "`MacdSignal`, ...) that project the protected `Real0`/`Real1`/`Real2` slots in output order." + ) + add("") + + add("## Known defects") + add("") + add( + "Three entry points below currently return wrong numbers. They fail quietly - `RetCode.Success`, a " + "plausible `NBElement`, and a value you would chart - so they are called out here and marked in the " + "tables that follow. Everything not listed here was checked against the same inputs and behaves." + ) + add("") + add("| Defect | Affected entry points | Detail |") + add("| --- | --- | --- |") + for _, title, affected, detail in DEFECT_ROWS: + add(f"| **{title}** | {affected} | {detail} |") + add("") + add( + "See [Getting started](../guides/getting-started.md#10-known-library-defects) for the two habits that " + "make these survivable, [Backtesting]" + "(../guides/backtesting.md#-limitations--read-before-believing-any-number) and " + "[Real-time streaming](../guides/real-time-streaming.md#-known-library-defects-visible-in-this-sample) " + "for what they do to a running system, and " + "[TradingView integration](../guides/tradingview-integration.md#0-known-library-defects) for what they " + "mean when you are reconciling against a chart." + ) + add("") + + for category, blurb, _ in CATEGORIES: + entries = buckets[category] + add(f"## {category}") + add("") + add(f"{blurb} **{len(entries)} entry points.**") + add("") + add("| Indicator | Signature | Parameters (with defaults) | Outputs | Description | Docs |") + add("| --- | --- | --- | --- | --- | --- |") + for entry in entries: + link = resolve_doc_link(repo_root, entry) + if link is None: + missing_links.append(f"{entry.holder}.{entry.name}") + docs_cell = "_not generated_" + else: + target, label = link + docs_cell = f"[{escape_cell(label)}]({target})" + add( + "| {name} | {signature} | {params} | {outputs} | {description} | {docs} |".format( + name=code(entry.name), + signature=signature_cell(entry), + params=tuning_parameters(entry.primary), + outputs=", ".join(code(o) for o in entry.outputs) or "_none_", + description=describe(entry), + docs=docs_cell, + ) + ) + add("") + + add("## See also") + add("") + add("- [Getting started](../guides/getting-started.md) - installation, `RetCode`/`BegIdx`/`NBElement`, alignment, pitfalls.") + add("- [TradingView integration](../guides/tradingview-integration.md) - Pine Script mapping, parity caveats, UDF feed, webhooks.") + add("- [`Atypical.TechnicalAnalysis.Functions` API reference](../functions/Atypical.TechnicalAnalysis.Functions.md)") + add("- [`Atypical.TechnicalAnalysis.Candles` API reference](../candles/Atypical.TechnicalAnalysis.Candles.md)") + add("- [`Atypical.TechnicalAnalysis.Common` API reference](../common/Atypical.TechnicalAnalysis.Common.md)") + add("") + + return "\n".join(out), missing_links + + +def slug(heading: str) -> str: + return re.sub(r"[^a-z0-9 -]", "", heading.lower()).replace(" ", "-") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--check", action="store_true", help="verify the catalog is up to date without writing") + parser.add_argument("--output", default=None, help="override the output path") + parser.add_argument("--quiet", action="store_true", help="suppress the progress report") + args = parser.parse_args(argv) + + repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + output_path = args.output or os.path.join(repo_root, "docs", "indicators", "README.md") + + entry_points = discover_functions(repo_root) + discover_candles(repo_root) + buckets = validate_mapping(entry_points) + content, missing_links = render(repo_root, entry_points, buckets) + + if not content.endswith("\n"): + content += "\n" + + if args.check: + try: + with open(output_path, encoding="utf-8", newline="") as handle: + current = handle.read() + except FileNotFoundError: + sys.stderr.write(f"ERROR: {output_path} does not exist. Run this script without --check.\n") + return 1 + if current != content: + sys.stderr.write(f"ERROR: {output_path} is out of date. Re-run this script without --check.\n") + return 1 + if not args.quiet: + sys.stdout.write(f"{os.path.relpath(output_path, repo_root)} is up to date.\n") + return 0 + + os.makedirs(os.path.dirname(output_path), exist_ok=True) + with open(output_path, "w", encoding="utf-8", newline="\n") as handle: + handle.write(content) + + if not args.quiet: + functions = [e for e in entry_points if e.holder == "TAMath"] + candles = [e for e in entry_points if e.holder == "TACandle"] + overloads = sum(len(e.overloads) for e in entry_points) + sys.stdout.write(f"TAMath entry points : {len(functions)}\n") + sys.stdout.write(f"TACandle entry points : {len(candles)}\n") + sys.stdout.write(f"Total entry points : {len(entry_points)}\n") + sys.stdout.write(f"Total public overloads: {overloads}\n") + for category, _, _ in CATEGORIES: + sys.stdout.write(f" {category:<24}: {len(buckets[category])}\n") + if missing_links: + sys.stdout.write( + f"WARNING: no generated documentation page found for {len(missing_links)} entry point(s): " + + ", ".join(missing_links) + + "\n" + ) + else: + sys.stdout.write(f"All {len(entry_points)} documentation links resolve to files on disk.\n") + sys.stdout.write(f"Wrote {os.path.relpath(output_path, repo_root)}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:]))