mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 02:28:05 +00:00
feat: add 8 new indicators with full integration
New indicators: - HWC (Holt-Winters Channel) — channels, 27 tests - VWMACD (Volume-Weighted MACD) — momentum, 38 tests - Squeeze Pro — oscillators, 69 tests - BW_MFI (Bill Williams MFI) — oscillators - DSTOCH (Double Stochastic) — oscillators - ATRSTOP (ATR Trailing Stop) — reversals - VSTOP (Volatility Stop) — reversals - Convexity (Beta Convexity) — statistics, 23 tests Integration: - Python bridge: Exports.cs, _bridge.py, wrapper modules - Documentation: _sidebar.md, _index.md pages, SPEC.md - All analyzer warnings fixed (MA0074, xUnit2013, S2699) Build: 0 warnings, 0 errors | Tests: 15,933 passed, 0 failed
This commit is contained in:
@@ -4,11 +4,12 @@ Statistical tools applied to price and returns. These indicators quantify relati
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [ADF](adf/Adf.md) | Augmented Dickey-Fuller Test | Unit root test for stationarity. MacKinnon p-value output [0,1]. |
|
||||
| [ACF](acf/Acf.md) | Autocorrelation Function | Correlation of time series with lagged copy. For ARMA model identification. |
|
||||
| [ADF](adf/Adf.md) | Augmented Dickey-Fuller Test | Unit root test for stationarity. MacKinnon p-value output [0,1]. |
|
||||
| [BETA](beta/Beta.md) | Beta Coefficient | Asset volatility relative to market. β=1 means market-matched risk. |
|
||||
| [CMA](cma/Cma.md) | Cumulative Moving Average | Running average of all values. Welford's algorithm. No window. |
|
||||
| [COINTEGRATION](cointegration/Cointegration.md) | Cointegration | Tests if series share long-term equilibrium. Pairs trading foundation. |
|
||||
| [CONVEXITY](convexity/Convexity.md) | Beta Convexity | Up/Down beta asymmetry. Convexity = (β⁺ - β⁻)². Measures payoff curvature. |
|
||||
| [CORREL](correl/Correl.md) | Correlation | Linear relationship between two variables. Range: -1 to +1. |
|
||||
| [COVARIANCE](covariance/Covariance.md) | Covariance | Joint variability of two random variables. Building block for β. |
|
||||
| [ENTROPY](entropy/Entropy.md) | Shannon Entropy | Measures uncertainty/randomness. Higher entropy = less predictable. |
|
||||
@@ -21,22 +22,22 @@ Statistical tools applied to price and returns. These indicators quantify relati
|
||||
| [KENDALL](kendall/Kendall.md) | Kendall Rank Correlation | Ordinal association. Robust to outliers. |
|
||||
| [KURTOSIS](kurtosis/Kurtosis.md) | Kurtosis | Tail heaviness. High kurtosis = fat tails = more extreme events. |
|
||||
| [LINREG](linreg/LinReg.md) | Linear Regression | Least squares fit. Outputs slope, intercept, R². |
|
||||
| [MEANDEV](meandev/MeanDev.md) | Mean Absolute Deviation | Outlier-robust dispersion. Core of CCI. MD ≈ 0.7979σ for normal data. |
|
||||
| [MEDIAN](median/Median.md) | Median | Middle value in sorted window. Robust to outliers. |
|
||||
| [MODE](mode/Mode.md) | Mode | Most frequent value. Use for categorical or discrete data. |
|
||||
| [PACF](pacf/Pacf.md) | Partial Autocorrelation Function | Direct correlation at lag k after removing intermediate effects. For AR model identification. |
|
||||
| [PERCENTILE](percentile/Percentile.md) | Percentile | Value below which given percentage of observations fall. |
|
||||
| [POLYFIT](polyfit/Polyfit.md) | Polynomial Fitting | Least-squares polynomial regression. |
|
||||
| [QUANTILE](quantile/Quantile.md) | Quantile | Divides distribution into equal probability intervals. |
|
||||
| [SKEW](skew/Skew.md) | Skewness | Distribution asymmetry. Positive: right tail. Negative: left tail. |
|
||||
| [SPEARMAN](spearman/Spearman.md) | Spearman Rank Correlation | Pearson on ranks. Measures monotonic relationship. |
|
||||
| [STDDEV](stddev/StdDev.md) | Standard Deviation | Square root of variance. Same units as data. |
|
||||
| [STDERR](stderr/Stderr.md) | Standard Error of Regression | OLS residual scatter over rolling window. Quantifies trend fit quality. |
|
||||
| [SUM](sum/Sum.md) | Rolling Sum | Kahan-Babuška summation. Numerically stable. |
|
||||
| [THEIL](theil/Theil.md) | Theil Index | Inequality measure. Decomposable into within/between group. |
|
||||
| [VARIANCE](variance/Variance.md) | Variance | Average squared deviation from mean. Units are squared. |
|
||||
| [ZSCORE](zscore/Zscore.md) | Z-Score | Standard deviations from mean. Normalizes different scales. |
|
||||
| [ZTEST](ztest/Ztest.md) | Z-Test | One-sample t-test statistic against hypothesized mean. |
|
||||
| [MEANDEV](meandev/MeanDev.md) | Mean Absolute Deviation | Outlier-robust dispersion. Core of CCI. MD ≈ 0.7979σ for normal data. |
|
||||
| [STDERR](stderr/Stderr.md) | Standard Error of Regression | OLS residual scatter over rolling window. Quantifies trend fit quality. |
|
||||
| [POLYFIT](polyfit/Polyfit.md) | Polynomial Fitting | Least-squares polynomial regression. |
|
||||
| [TRIM](trim/Trim.md) | Trimmed Mean MA | Mean after discarding extreme percentiles. |
|
||||
| [VARIANCE](variance/Variance.md) | Variance | Average squared deviation from mean. Units are squared. |
|
||||
| [WAVG](wavg/Wavg.md) | Weighted Average | Generic weighted mean. |
|
||||
| [WINS](wins/Wins.md) | Winsorized Mean MA | Mean with extreme values clamped to percentile bounds. |
|
||||
| [ZSCORE](zscore/Zscore.md) | Z-Score | Standard deviations from mean. Normalizes different scales. |
|
||||
| [ZTEST](ztest/Ztest.md) | Z-Test | One-sample t-test statistic against hypothesized mean. |
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class ConvexityIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 2, 2000, 1, 0)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Convexity _convexity = null!;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
// For dual-input, we use Close as asset and Open as market proxy
|
||||
// (In real use, user would customize the market data source)
|
||||
private readonly LineSeries _convexitySeries;
|
||||
private readonly LineSeries _betaStdSeries;
|
||||
private readonly LineSeries _betaUpSeries;
|
||||
private readonly LineSeries _betaDownSeries;
|
||||
private readonly LineSeries _ratioSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"CONVEXITY({Period}):{_sourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/statistics/convexity/Convexity.Quantower.cs";
|
||||
|
||||
public ConvexityIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
_sourceName = Source.ToString();
|
||||
Name = "CONVEXITY - Beta Convexity";
|
||||
Description = "Measures asymmetry between upside and downside beta relative to a market benchmark.";
|
||||
|
||||
_convexitySeries = new LineSeries("Convexity", Color.FromArgb(128, 128, 255), 2, LineStyle.Solid);
|
||||
_betaStdSeries = new LineSeries("Beta", Color.FromArgb(255, 255, 128), 1, LineStyle.Solid);
|
||||
_betaUpSeries = new LineSeries("Beta+", Color.FromArgb(128, 255, 128), 1, LineStyle.Dash);
|
||||
_betaDownSeries = new LineSeries("Beta-", Color.FromArgb(255, 128, 128), 1, LineStyle.Dash);
|
||||
_ratioSeries = new LineSeries("Ratio", Color.FromArgb(255, 165, 0), 1, LineStyle.Dot);
|
||||
|
||||
AddLineSeries(_convexitySeries);
|
||||
AddLineSeries(_betaStdSeries);
|
||||
AddLineSeries(_betaUpSeries);
|
||||
AddLineSeries(_betaDownSeries);
|
||||
AddLineSeries(_ratioSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_convexity = new Convexity(Period);
|
||||
_sourceName = Source.ToString();
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
|
||||
// Use selected source as asset, and Open as market proxy
|
||||
double assetPrice = _priceSelector(item);
|
||||
double marketPrice = item[PriceType.Open];
|
||||
|
||||
_convexity.Update(
|
||||
new TValue(item.TimeLeft.Ticks, assetPrice),
|
||||
new TValue(item.TimeLeft.Ticks, marketPrice),
|
||||
args.IsNewBar());
|
||||
|
||||
bool isHot = _convexity.IsHot;
|
||||
_convexitySeries.SetValue(_convexity.ConvexityValue, isHot, ShowColdValues);
|
||||
_betaStdSeries.SetValue(_convexity.BetaStd, isHot, ShowColdValues);
|
||||
_betaUpSeries.SetValue(_convexity.BetaUp, isHot, ShowColdValues);
|
||||
_betaDownSeries.SetValue(_convexity.BetaDown, isHot, ShowColdValues);
|
||||
_ratioSeries.SetValue(_convexity.Ratio, isHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
// Convexity: Beta Convexity (Markowitz Up/Down Beta asymmetry measure)
|
||||
// Measures the squared difference between Upside Beta and Downside Beta.
|
||||
// Based on Skender's GetBeta(BetaType.All) implementation.
|
||||
// Popularized by Harry M. Markowitz in portfolio theory.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using static System.Math;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Beta Convexity: Measures the asymmetry between upside and downside beta
|
||||
/// of an asset relative to a market benchmark.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Algorithm (5 outputs):
|
||||
/// 1. Standard Beta = Cov(Ra, Rm) / Var(Rm) — all bars
|
||||
/// 2. BetaUp (β⁺) = Cov(Ra, Rm) / Var(Rm) — only market up bars (Rm > 0)
|
||||
/// 3. BetaDown (β⁻) = Cov(Ra, Rm) / Var(Rm) — only market down bars (Rm < 0)
|
||||
/// 4. Ratio = β⁺ / β⁻
|
||||
/// 5. Convexity = (β⁺ - β⁻)²
|
||||
///
|
||||
/// Returns are simple percentage returns: R[i] = (P[i] - P[i-1]) / P[i-1]
|
||||
///
|
||||
/// Standard Beta uses O(1) Kahan compensated running sums.
|
||||
/// Up/Down Beta uses O(period) window scan per update (clean, correct for typical periods 20-60).
|
||||
///
|
||||
/// Reference: Skender.Stock.Indicators GetBeta() with BetaType.All
|
||||
/// https://dotnet.stockindicators.dev/indicators/Beta/
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Convexity : AbstractBase
|
||||
{
|
||||
private readonly RingBuffer _returnsAsset;
|
||||
private readonly RingBuffer _returnsMarket;
|
||||
|
||||
private double _prevAsset;
|
||||
private double _prevMarket;
|
||||
private double _p_prevAsset;
|
||||
private double _p_prevMarket;
|
||||
private bool _isInitialized;
|
||||
|
||||
// O(1) Kahan compensated running sums for standard beta
|
||||
private double _sumRa, _sumRm, _sumRaRm, _sumRm2;
|
||||
private double _sumRaComp, _sumRmComp, _sumRaRmComp, _sumRm2Comp;
|
||||
|
||||
// Previous compensation state for bar correction (match Beta.cs pattern)
|
||||
private double _p_sumRaComp, _p_sumRmComp, _p_sumRaRmComp, _p_sumRm2Comp;
|
||||
|
||||
private const double Epsilon = 1e-10;
|
||||
|
||||
/// <summary>True when the lookback window is fully populated.</summary>
|
||||
public override bool IsHot => _returnsAsset.IsFull;
|
||||
|
||||
/// <summary>Lookback period.</summary>
|
||||
public int Period => _returnsAsset.Capacity;
|
||||
|
||||
/// <summary>Standard beta coefficient (all bars).</summary>
|
||||
public double BetaStd { get; private set; }
|
||||
|
||||
/// <summary>Upside beta — computed from market up bars only (Rm > 0).</summary>
|
||||
public double BetaUp { get; private set; }
|
||||
|
||||
/// <summary>Downside beta — computed from market down bars only (Rm < 0).</summary>
|
||||
public double BetaDown { get; private set; }
|
||||
|
||||
/// <summary>Beta ratio = BetaUp / BetaDown.</summary>
|
||||
public double Ratio { get; private set; }
|
||||
|
||||
/// <summary>Beta convexity = (BetaUp - BetaDown)². Always ≥ 0.</summary>
|
||||
public double ConvexityValue { get; private set; }
|
||||
|
||||
/// <param name="period">Lookback period (must be ≥ 2). Institutions use 60 for 5-year monthly data.</param>
|
||||
public Convexity(int period = 20)
|
||||
{
|
||||
if (period < 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(period), "Period must be at least 2.");
|
||||
}
|
||||
|
||||
_returnsAsset = new RingBuffer(period);
|
||||
_returnsMarket = new RingBuffer(period);
|
||||
Name = $"Convexity({period})";
|
||||
WarmupPeriod = period + 1; // Need 1 extra bar for first return
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates with new asset and market prices.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue asset, TValue market, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
return ProcessNewBar(asset, market);
|
||||
}
|
||||
else
|
||||
{
|
||||
return ProcessBarCorrection(asset, market);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates with raw double values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(double asset, double market, bool isNew = true)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
return Update(new TValue(now, asset), new TValue(now, market), isNew);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
throw new NotSupportedException("Convexity requires two inputs (asset and market). Use Update(asset, market).");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
throw new NotSupportedException("Convexity requires two inputs (asset and market). Use Batch(assetSeries, marketSeries, period).");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
throw new NotSupportedException("Convexity requires two inputs (asset and market).");
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue ProcessNewBar(TValue asset, TValue market)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
_isInitialized = true;
|
||||
Last = new TValue(asset.Time, 0);
|
||||
PubEvent(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
// Snapshot compensation state for bar correction (match Beta.cs pattern)
|
||||
_p_prevAsset = _prevAsset;
|
||||
_p_prevMarket = _prevMarket;
|
||||
_p_sumRaComp = _sumRaComp;
|
||||
_p_sumRmComp = _sumRmComp;
|
||||
_p_sumRaRmComp = _sumRaRmComp;
|
||||
_p_sumRm2Comp = _sumRm2Comp;
|
||||
|
||||
// Calculate returns
|
||||
double ra = ComputeReturn(asset.Value, _prevAsset);
|
||||
double rm = ComputeReturn(market.Value, _prevMarket);
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
|
||||
// Evict oldest from running sums if buffer full
|
||||
if (_returnsAsset.IsFull)
|
||||
{
|
||||
double oldRa = _returnsAsset.Oldest;
|
||||
double oldRm = _returnsMarket.Oldest;
|
||||
KahanSubtract(ref _sumRa, ref _sumRaComp, oldRa);
|
||||
KahanSubtract(ref _sumRm, ref _sumRmComp, oldRm);
|
||||
KahanSubtract(ref _sumRaRm, ref _sumRaRmComp, oldRa * oldRm);
|
||||
KahanSubtract(ref _sumRm2, ref _sumRm2Comp, oldRm * oldRm);
|
||||
}
|
||||
|
||||
_returnsAsset.Add(ra);
|
||||
_returnsMarket.Add(rm);
|
||||
|
||||
// Add new to running sums
|
||||
KahanAdd(ref _sumRa, ref _sumRaComp, ra);
|
||||
KahanAdd(ref _sumRm, ref _sumRmComp, rm);
|
||||
KahanAdd(ref _sumRaRm, ref _sumRaRmComp, ra * rm);
|
||||
KahanAdd(ref _sumRm2, ref _sumRm2Comp, rm * rm);
|
||||
|
||||
ComputeOutputs(asset.Time);
|
||||
return Last;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private TValue ProcessBarCorrection(TValue asset, TValue market)
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
_isInitialized = true;
|
||||
Last = new TValue(asset.Time, 0);
|
||||
PubEvent(Last, false);
|
||||
return Last;
|
||||
}
|
||||
|
||||
if (_returnsAsset.Count == 0)
|
||||
{
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
_p_prevAsset = asset.Value;
|
||||
_p_prevMarket = market.Value;
|
||||
Last = new TValue(asset.Time, 0);
|
||||
PubEvent(Last, false);
|
||||
return Last;
|
||||
}
|
||||
|
||||
// Restore only compensation state (match Beta.cs pattern)
|
||||
// Sums already contain the old bar's values — delta will swap them
|
||||
_sumRaComp = _p_sumRaComp;
|
||||
_sumRmComp = _p_sumRmComp;
|
||||
_sumRaRmComp = _p_sumRaRmComp;
|
||||
_sumRm2Comp = _p_sumRm2Comp;
|
||||
|
||||
double oldRa = _returnsAsset.Newest;
|
||||
double oldRm = _returnsMarket.Newest;
|
||||
|
||||
// Calculate new returns from restored previous prices
|
||||
double newRa = ComputeReturn(asset.Value, _p_prevAsset);
|
||||
double newRm = ComputeReturn(market.Value, _p_prevMarket);
|
||||
_prevAsset = asset.Value;
|
||||
_prevMarket = market.Value;
|
||||
|
||||
_returnsAsset.UpdateNewest(newRa);
|
||||
_returnsMarket.UpdateNewest(newRm);
|
||||
|
||||
// Kahan delta update: subtract old + add new (sums still contain old values)
|
||||
KahanDelta(ref _sumRa, ref _sumRaComp, oldRa, newRa);
|
||||
KahanDelta(ref _sumRm, ref _sumRmComp, oldRm, newRm);
|
||||
KahanDelta(ref _sumRaRm, ref _sumRaRmComp, oldRa * oldRm, newRa * newRm);
|
||||
KahanDelta(ref _sumRm2, ref _sumRm2Comp, oldRm * oldRm, newRm * newRm);
|
||||
|
||||
ComputeOutputs(asset.Time);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes all 5 outputs from current state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ComputeOutputs(long time)
|
||||
{
|
||||
int n = _returnsAsset.Count;
|
||||
if (n < 2)
|
||||
{
|
||||
BetaStd = 0;
|
||||
BetaUp = 0;
|
||||
BetaDown = 0;
|
||||
Ratio = 0;
|
||||
ConvexityValue = 0;
|
||||
Last = new TValue(time, 0);
|
||||
PubEvent(Last);
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard Beta — O(1) from running sums
|
||||
BetaStd = ComputeBetaFromSums(n, _sumRa, _sumRm, _sumRaRm, _sumRm2);
|
||||
|
||||
// Up/Down Beta — O(period) scan
|
||||
ComputeFilteredBetas();
|
||||
|
||||
// Derived outputs
|
||||
if (Abs(BetaDown) > Epsilon)
|
||||
{
|
||||
Ratio = BetaUp / BetaDown;
|
||||
}
|
||||
else
|
||||
{
|
||||
Ratio = 0;
|
||||
}
|
||||
|
||||
double diff = BetaUp - BetaDown;
|
||||
ConvexityValue = diff * diff;
|
||||
|
||||
Last = new TValue(time, ConvexityValue);
|
||||
PubEvent(Last);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes beta from Kahan running sums using FMA.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeBetaFromSums(int n, double sumRa, double sumRm, double sumRaRm, double sumRm2)
|
||||
{
|
||||
// Beta = (N * Σ(Ra*Rm) - ΣRa * ΣRm) / (N * Σ(Rm²) - (ΣRm)²)
|
||||
double denom = FusedMultiplyAdd(n, sumRm2, -sumRm * sumRm);
|
||||
if (Abs(denom) <= Epsilon)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double numer = FusedMultiplyAdd(n, sumRaRm, -sumRa * sumRm);
|
||||
return numer / denom;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans ring buffers to compute Up Beta and Down Beta.
|
||||
/// O(period) per call — clean and correct for typical lookback windows.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void ComputeFilteredBetas()
|
||||
{
|
||||
int n = _returnsAsset.Count;
|
||||
|
||||
double sumRaUp = 0, sumRmUp = 0, sumRaRmUp = 0, sumRm2Up = 0;
|
||||
double sumRaDn = 0, sumRmDn = 0, sumRaRmDn = 0, sumRm2Dn = 0;
|
||||
int countUp = 0, countDn = 0;
|
||||
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
double ra = _returnsAsset[i];
|
||||
double rm = _returnsMarket[i];
|
||||
|
||||
if (rm > 0)
|
||||
{
|
||||
sumRaUp += ra;
|
||||
sumRmUp += rm;
|
||||
sumRaRmUp += ra * rm;
|
||||
sumRm2Up += rm * rm;
|
||||
countUp++;
|
||||
}
|
||||
else if (rm < 0)
|
||||
{
|
||||
sumRaDn += ra;
|
||||
sumRmDn += rm;
|
||||
sumRaRmDn += ra * rm;
|
||||
sumRm2Dn += rm * rm;
|
||||
countDn++;
|
||||
}
|
||||
// rm == 0 bars excluded from both (same as Skender)
|
||||
}
|
||||
|
||||
BetaUp = countUp >= 2
|
||||
? ComputeBetaFromSums(countUp, sumRaUp, sumRmUp, sumRaRmUp, sumRm2Up)
|
||||
: 0;
|
||||
|
||||
BetaDown = countDn >= 2
|
||||
? ComputeBetaFromSums(countDn, sumRaDn, sumRmDn, sumRaRmDn, sumRm2Dn)
|
||||
: 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes simple return with division-by-zero and NaN/Infinity guards.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeReturn(double current, double previous)
|
||||
{
|
||||
if (Abs(previous) < Epsilon)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
double r = (current - previous) / previous;
|
||||
return double.IsFinite(r) ? r : 0;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void KahanAdd(ref double sum, ref double comp, double value)
|
||||
{
|
||||
double y = value - comp;
|
||||
double t = sum + y;
|
||||
comp = (t - sum) - y;
|
||||
sum = t;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void KahanSubtract(ref double sum, ref double comp, double value)
|
||||
{
|
||||
double y = -value - comp;
|
||||
double t = sum + y;
|
||||
comp = (t - sum) - y;
|
||||
sum = t;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void KahanDelta(ref double sum, ref double comp, double oldVal, double newVal)
|
||||
{
|
||||
double y = (newVal - oldVal) - comp;
|
||||
double t = sum + y;
|
||||
comp = (t - sum) - y;
|
||||
sum = t;
|
||||
}
|
||||
|
||||
// --- Static batch API ---
|
||||
|
||||
/// <summary>
|
||||
/// Batch computation of Convexity from two price series (TSeries).
|
||||
/// Returns tuple of (BetaStd, BetaUp, BetaDown, Ratio, Convexity) series.
|
||||
/// </summary>
|
||||
public static (TSeries BetaStd, TSeries BetaUp, TSeries BetaDown, TSeries Ratio, TSeries Convexity) Batch(
|
||||
TSeries assetPrices, TSeries marketPrices, int period = 20)
|
||||
{
|
||||
if (assetPrices.Count != marketPrices.Count)
|
||||
{
|
||||
throw new ArgumentException("Asset and market series must have the same length.", nameof(marketPrices));
|
||||
}
|
||||
|
||||
int len = assetPrices.Count;
|
||||
var indicator = new Convexity(period);
|
||||
|
||||
var betaStdList = new TSeries(len);
|
||||
var betaUpList = new TSeries(len);
|
||||
var betaDownList = new TSeries(len);
|
||||
var ratioList = new TSeries(len);
|
||||
var convexityList = new TSeries(len);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
TValue asset = assetPrices[i];
|
||||
TValue market = marketPrices[i];
|
||||
indicator.Update(asset, market, isNew: true);
|
||||
|
||||
long t = asset.Time;
|
||||
betaStdList.Add(new TValue(t, indicator.BetaStd));
|
||||
betaUpList.Add(new TValue(t, indicator.BetaUp));
|
||||
betaDownList.Add(new TValue(t, indicator.BetaDown));
|
||||
ratioList.Add(new TValue(t, indicator.Ratio));
|
||||
convexityList.Add(new TValue(t, indicator.ConvexityValue));
|
||||
}
|
||||
|
||||
return (betaStdList, betaUpList, betaDownList, ratioList, convexityList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Span-based batch computation for NativeAOT bridge.
|
||||
/// Writes 5 output spans: betaStd, betaUp, betaDown, ratio, convexity.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> asset, ReadOnlySpan<double> market,
|
||||
Span<double> betaStd, Span<double> betaUp, Span<double> betaDown,
|
||||
Span<double> ratio, Span<double> convexity, int period = 20)
|
||||
{
|
||||
int len = asset.Length;
|
||||
var indicator = new Convexity(period);
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
indicator.Update(asset[i], market[i]);
|
||||
betaStd[i] = indicator.BetaStd;
|
||||
betaUp[i] = indicator.BetaUp;
|
||||
betaDown[i] = indicator.BetaDown;
|
||||
ratio[i] = indicator.Ratio;
|
||||
convexity[i] = indicator.ConvexityValue;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_returnsAsset.Clear();
|
||||
_returnsMarket.Clear();
|
||||
_sumRa = 0; _sumRm = 0; _sumRaRm = 0; _sumRm2 = 0;
|
||||
_sumRaComp = 0; _sumRmComp = 0; _sumRaRmComp = 0; _sumRm2Comp = 0;
|
||||
_p_sumRaComp = 0; _p_sumRmComp = 0; _p_sumRaRmComp = 0; _p_sumRm2Comp = 0;
|
||||
_prevAsset = 0; _prevMarket = 0;
|
||||
_p_prevAsset = 0; _p_prevMarket = 0;
|
||||
_isInitialized = false;
|
||||
BetaStd = 0; BetaUp = 0; BetaDown = 0; Ratio = 0; ConvexityValue = 0;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
# Convexity: Beta Convexity
|
||||
|
||||
> *The asymmetry between upside and downside beta reveals whether an asset delivers convex payoffs — the holy grail of portfolio construction.*
|
||||
|
||||
| Property | Value |
|
||||
| ---------------- | -------------------------------- |
|
||||
| **Category** | Statistic |
|
||||
| **Inputs** | Dual (asset price, market price) |
|
||||
| **Parameters** | `period` |
|
||||
| **Outputs** | 5 series (BetaStd, BetaUp, BetaDown, Ratio, Convexity) |
|
||||
| **Output range** | Convexity ≥ 0; BetaStd/Up/Down unbounded |
|
||||
| **Warmup** | `period + 1` bars |
|
||||
|
||||
- Convexity measures how asymmetrically an asset responds to market up-moves vs. down-moves.
|
||||
- **Similar:** [Beta](../beta/Beta.md), [Correl](../correl/Correl.md) | **Trading note:** Convexity > 0 signals favorable payoff asymmetry. Ratio > 1 = asset amplifies gains more than losses.
|
||||
- Based on Skender.Stock.Indicators `GetBeta(BetaType.All)` implementation.
|
||||
|
||||
Beta Convexity decomposes the standard beta coefficient into its upside and downside components, then measures their squared difference. An asset with positive convexity captures more upside than downside — the ideal characteristic for portfolio construction. Harry Markowitz's Modern Portfolio Theory shows that investors should seek assets that maximise `(β⁺ - β⁻)²`.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The concept of separating upside and downside beta was pioneered by Bawa and Lindenberg (1977) in their work on lower partial moments. It gained mainstream traction through Ang, Chen, and Xing's landmark 2006 paper "Downside Risk," which demonstrated that stocks with high downside beta earn higher returns — the so-called "downside risk premium." Skender's .NET implementation packages this as `BetaType.All`, computing standard, upside, downside, ratio, and convexity in a single pass.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
Convexity is built on the same dual-input pattern as Beta, but adds a conditional filtering step:
|
||||
|
||||
1. **Standard Beta** uses O(1) Kahan-compensated running sums for `Cov(Ra, Rm) / Var(Rm)`
|
||||
2. **Filtered Betas** perform an O(period) scan of the ring buffer, partitioning returns by market direction:
|
||||
- `Rm > 0` → contributes to BetaUp sums
|
||||
- `Rm < 0` → contributes to BetaDown sums
|
||||
- `Rm = 0` → excluded (following Skender's convention)
|
||||
|
||||
### The Bar Correction Pattern
|
||||
|
||||
For streaming bar corrections (`isNew = false`), Convexity follows the proven Beta.cs pattern: only compensation values are saved/restored (not full sums), and a Kahan delta swaps the old return contribution for the new one. This ensures numerical stability across long-running sessions.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Standard Beta (all bars)
|
||||
|
||||
$$ \beta = \frac{N \sum R_a R_m - \sum R_a \sum R_m}{N \sum R_m^2 - (\sum R_m)^2} $$
|
||||
|
||||
### Upside Beta (market up bars only, $R_m > 0$)
|
||||
|
||||
$$ \beta^+ = \frac{N^+ \sum_{R_m > 0} R_a R_m - \sum_{R_m > 0} R_a \sum_{R_m > 0} R_m}{N^+ \sum_{R_m > 0} R_m^2 - (\sum_{R_m > 0} R_m)^2} $$
|
||||
|
||||
### Downside Beta (market down bars only, $R_m < 0$)
|
||||
|
||||
$$ \beta^- = \frac{N^- \sum_{R_m < 0} R_a R_m - \sum_{R_m < 0} R_a \sum_{R_m < 0} R_m}{N^- \sum_{R_m < 0} R_m^2 - (\sum_{R_m < 0} R_m)^2} $$
|
||||
|
||||
### Derived Outputs
|
||||
|
||||
$$ \text{Ratio} = \frac{\beta^+}{\beta^-} $$
|
||||
|
||||
$$ \text{Convexity} = (\beta^+ - \beta^-)^2 $$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Ring buffer add/evict (2 inputs) | 2 | 3 cy | ~6 cy |
|
||||
| Compute asset + market returns | 2 | 3 cy | ~6 cy |
|
||||
| Update 4 Kahan running sums | 4 | 4 cy | ~16 cy |
|
||||
| Compute standard beta (FMA) | 1 | 5 cy | ~5 cy |
|
||||
| O(period) scan for Up/Down beta | period | 4 cy | ~80 cy* |
|
||||
| Compute ratio + convexity | 2 | 3 cy | ~6 cy |
|
||||
| **Total** | **O(period)** | — | **~119 cy** |
|
||||
|
||||
*Assuming period = 20. The O(period) scan is a simple branch-free iteration with no allocations.
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **Throughput** | ~40 ns/bar | O(period) scan dominates. |
|
||||
| **Allocations** | 0 | Zero-allocation hot path. |
|
||||
| **Complexity** | O(period) | Linear scan for up/down filtering. |
|
||||
| **Accuracy** | 9 | Kahan compensation prevents drift. |
|
||||
| **Timeliness** | Lagged | Depends on the lookback period. |
|
||||
| **Smoothness** | Low | Sensitive to period and market regime. |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :--- | :--- |
|
||||
| **QuanTAlib** | ✅ | Reference implementation. |
|
||||
| **Skender** | ✅ | Matches `GetBeta(BetaType.All)` — 5-output bundle. |
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
1. **Zero Variance in Up/Down Subsets**: If all market up-day returns are identical, `Var(Rm|Rm>0) = 0` and BetaUp is undefined (returns 0). This occurs with synthetic data; real market data always has variance.
|
||||
2. **Period Sensitivity**: Short periods (< 10) may have too few up/down bars for meaningful beta decomposition. Typical institutional use: period = 60 (3-month daily).
|
||||
3. **Interpretation**: Convexity = 0 does NOT mean beta = 0. It means upside and downside betas are equal (symmetric risk profile).
|
||||
|
||||
## C# Usage
|
||||
|
||||
```csharp
|
||||
// Initialize with period 20
|
||||
var conv = new Convexity(20);
|
||||
|
||||
// Update with Asset and Market prices
|
||||
conv.Update(assetPrice, marketPrice);
|
||||
|
||||
Console.WriteLine($"BetaStd: {conv.BetaStd:F4}");
|
||||
Console.WriteLine($"BetaUp: {conv.BetaUp:F4}");
|
||||
Console.WriteLine($"BetaDown: {conv.BetaDown:F4}");
|
||||
Console.WriteLine($"Ratio: {conv.Ratio:F4}");
|
||||
Console.WriteLine($"Convexity: {conv.ConvexityValue:F4}");
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
```csharp
|
||||
var (betaStd, betaUp, betaDown, ratio, convexity) =
|
||||
Convexity.Batch(assetSeries, marketSeries, period: 20);
|
||||
```
|
||||
|
||||
### Bar Correction
|
||||
|
||||
```csharp
|
||||
// New bar
|
||||
conv.Update(assetPrice, marketPrice, isNew: true);
|
||||
|
||||
// Update same bar (price correction)
|
||||
conv.Update(correctedAsset, correctedMarket, isNew: false);
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
- [Skender GetBeta](https://dotnet.stockindicators.dev/indicators/Beta/) — BetaType.All returns all 5 outputs.
|
||||
- Ang, Chen, Xing (2006). ["Downside Risk"](https://academic.oup.com/rfs/article/19/4/1191/1572624) — Empirical evidence for downside risk premium.
|
||||
- Bawa, Lindenberg (1977). "Capital Market Equilibrium in a Mean-Lower Partial Moment Framework" — Original lower partial moment theory.
|
||||
@@ -0,0 +1,82 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class ConvexityIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_SetsDefaults()
|
||||
{
|
||||
var ind = new ConvexityIndicator();
|
||||
Assert.Equal("CONVEXITY - Beta Convexity", ind.Name);
|
||||
Assert.True(ind.SeparateWindow);
|
||||
Assert.Equal(20, ind.Period);
|
||||
Assert.True(ind.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
Assert.Equal(0, ConvexityIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator w = new ConvexityIndicator();
|
||||
Assert.Equal(0, w.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ShortName_IncludesParameters()
|
||||
{
|
||||
var ind = new ConvexityIndicator { Period = 30 };
|
||||
ind.Initialize();
|
||||
Assert.Contains("CONVEXITY", ind.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("30", ind.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceCodeLink_IsValid()
|
||||
{
|
||||
var ind = new ConvexityIndicator();
|
||||
Assert.Contains("github.com", ind.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Convexity.Quantower.cs", ind.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_CreatesLineSeries()
|
||||
{
|
||||
var ind = new ConvexityIndicator();
|
||||
ind.Initialize();
|
||||
Assert.Equal(5, ind.LinesSeries.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var ind = new ConvexityIndicator { Period = 5 };
|
||||
ind.Initialize();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
ind.HistoricalData.AddBar(
|
||||
DateTime.UtcNow.AddMinutes(i),
|
||||
100 + i * 0.5, 101 + i * 0.5, 99 + i * 0.5, 100.5 + i * 0.5, 1000);
|
||||
}
|
||||
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double val = ind.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var ind = new ConvexityIndicator { Period = 3 };
|
||||
ind.Initialize();
|
||||
|
||||
ind.HistoricalData.AddBar(DateTime.UtcNow, 100, 101, 99, 100.5, 1000);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
ind.HistoricalData.AddBar(DateTime.UtcNow.AddMinutes(1), 101, 102, 100, 101.5, 1100);
|
||||
ind.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.True(double.IsFinite(ind.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public sealed class ConvexityTests
|
||||
{
|
||||
// ── A. Constructor & Properties ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ValidatesPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Convexity(1));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Convexity(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Convexity(-1));
|
||||
|
||||
var c = new Convexity(2);
|
||||
Assert.NotNull(c);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod()
|
||||
{
|
||||
var c = new Convexity();
|
||||
Assert.Equal(20, c.Period);
|
||||
Assert.Equal(21, c.WarmupPeriod); // period + 1
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod()
|
||||
{
|
||||
var c = new Convexity(60);
|
||||
Assert.Equal(60, c.Period);
|
||||
Assert.Equal(61, c.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Properties_InitialState()
|
||||
{
|
||||
var c = new Convexity(10);
|
||||
Assert.Equal(0, c.Last.Value);
|
||||
Assert.Equal(0, c.BetaStd);
|
||||
Assert.Equal(0, c.BetaUp);
|
||||
Assert.Equal(0, c.BetaDown);
|
||||
Assert.Equal(0, c.Ratio);
|
||||
Assert.Equal(0, c.ConvexityValue);
|
||||
Assert.False(c.IsHot);
|
||||
Assert.Contains("Convexity", c.Name, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleInput_ThrowsNotSupported()
|
||||
{
|
||||
var c = new Convexity(10);
|
||||
Assert.Throws<NotSupportedException>(() => c.Update(new TValue(DateTime.UtcNow, 100)));
|
||||
Assert.Throws<NotSupportedException>(() => c.Update(new TSeries()));
|
||||
Assert.Throws<NotSupportedException>(() => c.Prime([1, 2, 3]));
|
||||
}
|
||||
|
||||
// ── B. IsHot warmup ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterPeriodPlusOne()
|
||||
{
|
||||
const int period = 5;
|
||||
var c = new Convexity(period);
|
||||
|
||||
// First update initializes prev prices, no return computed yet
|
||||
for (int i = 0; i <= period; i++)
|
||||
{
|
||||
Assert.False(c.IsHot, $"IsHot should be false at index {i}");
|
||||
c.Update(100.0 + i, 100.0 + i);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot, "IsHot should be true after period+1 updates");
|
||||
}
|
||||
|
||||
// ── C. Known values: symmetric beta ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void SymmetricBeta_ConvexityIsZero()
|
||||
{
|
||||
// Asset = 2x market returns in BOTH directions
|
||||
// Use varying magnitudes so up/down subsets have variance
|
||||
// → BetaUp ≈ 2, BetaDown ≈ 2 → Convexity ≈ 0
|
||||
const int period = 10;
|
||||
var c = new Convexity(period);
|
||||
var rng = new Random(42);
|
||||
|
||||
double mkt = 100;
|
||||
double ast = 100;
|
||||
c.Update(ast, mkt);
|
||||
|
||||
for (int i = 1; i <= period; i++)
|
||||
{
|
||||
double sign = (i % 2 == 0) ? 1 : -1;
|
||||
double magnitude = 0.01 + rng.NextDouble() * 0.03;
|
||||
double mktReturn = sign * magnitude;
|
||||
mkt *= (1 + mktReturn);
|
||||
ast *= (1 + 2 * mktReturn); // exactly 2x market return
|
||||
c.Update(ast, mkt);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot);
|
||||
Assert.True(c.BetaStd > 1.5, $"BetaStd={c.BetaStd} should be near 2");
|
||||
Assert.True(c.BetaStd < 2.5, $"BetaStd={c.BetaStd} should be near 2");
|
||||
Assert.True(c.ConvexityValue < 0.5, $"Convexity={c.ConvexityValue} should be near 0 for symmetric beta");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AsymmetricBeta_ConvexityIsPositive()
|
||||
{
|
||||
// Asset amplifies gains (3x up) but dampens losses (1x down)
|
||||
// Use VARYING magnitude returns so there's variance within up/down subsets
|
||||
// (identical magnitudes → zero variance → beta undefined)
|
||||
const int period = 20;
|
||||
var c = new Convexity(period);
|
||||
var rng = new Random(42);
|
||||
|
||||
double mkt = 100;
|
||||
double ast = 100;
|
||||
c.Update(ast, mkt);
|
||||
|
||||
for (int i = 1; i <= period; i++)
|
||||
{
|
||||
double sign = (i % 2 == 0) ? 1 : -1;
|
||||
double magnitude = 0.01 + rng.NextDouble() * 0.03; // 1%-4% varying
|
||||
double mktReturn = sign * magnitude;
|
||||
double astReturn;
|
||||
if (mktReturn > 0)
|
||||
{
|
||||
astReturn = 3 * mktReturn; // 3x on up days
|
||||
}
|
||||
else
|
||||
{
|
||||
astReturn = 1 * mktReturn; // 1x on down days
|
||||
}
|
||||
mkt *= (1 + mktReturn);
|
||||
ast *= (1 + astReturn);
|
||||
c.Update(ast, mkt);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot);
|
||||
Assert.True(c.BetaUp > 2.0, $"BetaUp={c.BetaUp} should be near 3");
|
||||
Assert.True(c.BetaDown > 0.5, $"BetaDown={c.BetaDown} should be near 1");
|
||||
Assert.True(c.ConvexityValue > 1.0, $"Convexity={c.ConvexityValue} should be > 1 for asymmetric beta");
|
||||
Assert.True(c.Ratio > 1.0, $"Ratio={c.Ratio} should be > 1 (favorable asymmetry)");
|
||||
}
|
||||
|
||||
// ── D. Convexity is always non-negative ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConvexityIsAlwaysNonNegative()
|
||||
{
|
||||
var c = new Convexity(10);
|
||||
var rng = new Random(42);
|
||||
|
||||
c.Update(100.0, 100.0);
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double ast = 100.0 + rng.NextDouble() * 20 - 10;
|
||||
double mkt = 100.0 + rng.NextDouble() * 20 - 10;
|
||||
c.Update(ast, mkt);
|
||||
Assert.True(c.ConvexityValue >= 0, $"Convexity must be ≥ 0, got {c.ConvexityValue} at i={i}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── E. Identical series → Beta = 1, Convexity = 0 ────────────────────
|
||||
|
||||
[Fact]
|
||||
public void IdenticalSeries_BetaIsOne()
|
||||
{
|
||||
const int period = 10;
|
||||
var c = new Convexity(period);
|
||||
var rng = new Random(42);
|
||||
|
||||
double price = 100;
|
||||
c.Update(price, price);
|
||||
|
||||
for (int i = 1; i <= period + 5; i++)
|
||||
{
|
||||
double sign = (i % 2 == 0) ? 1 : -1;
|
||||
double magnitude = 0.005 + rng.NextDouble() * 0.02;
|
||||
price *= (1 + sign * magnitude);
|
||||
c.Update(price, price);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot);
|
||||
Assert.True(Math.Abs(c.BetaStd - 1.0) < 0.01, $"BetaStd={c.BetaStd} should be 1.0 for identical series");
|
||||
}
|
||||
|
||||
// ── F. Reset ─────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var c = new Convexity(5);
|
||||
c.Update(100.0, 100.0);
|
||||
c.Update(101.0, 101.0);
|
||||
c.Update(102.0, 102.0);
|
||||
|
||||
c.Reset();
|
||||
|
||||
Assert.False(c.IsHot);
|
||||
Assert.Equal(0, c.BetaStd);
|
||||
Assert.Equal(0, c.BetaUp);
|
||||
Assert.Equal(0, c.BetaDown);
|
||||
Assert.Equal(0, c.Ratio);
|
||||
Assert.Equal(0, c.ConvexityValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_RestartsCleanly()
|
||||
{
|
||||
var c = new Convexity(5);
|
||||
|
||||
// First run
|
||||
c.Update(100.0, 100.0);
|
||||
for (int i = 1; i <= 6; i++)
|
||||
{
|
||||
c.Update(100.0 + i, 100.0 + i);
|
||||
}
|
||||
double firstBeta = c.BetaStd;
|
||||
|
||||
// Reset and run again with same data
|
||||
c.Reset();
|
||||
c.Update(100.0, 100.0);
|
||||
for (int i = 1; i <= 6; i++)
|
||||
{
|
||||
c.Update(100.0 + i, 100.0 + i);
|
||||
}
|
||||
double secondBeta = c.BetaStd;
|
||||
|
||||
Assert.Equal(firstBeta, secondBeta, 10);
|
||||
}
|
||||
|
||||
// ── G. Bar correction ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_UpdatesSameBar()
|
||||
{
|
||||
var c = new Convexity(5);
|
||||
|
||||
c.Update(100.0, 100.0);
|
||||
c.Update(101.0, 101.0);
|
||||
c.Update(102.0, 102.0);
|
||||
|
||||
// Correct last bar
|
||||
c.Update(103.0, 103.0, isNew: false);
|
||||
|
||||
// Should not crash, and should produce a valid result
|
||||
Assert.True(double.IsFinite(c.ConvexityValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BarCorrection_MatchesFreshCalculation()
|
||||
{
|
||||
const int period = 5;
|
||||
|
||||
// Path A: feed N bars, then update last bar with correction
|
||||
var cA = new Convexity(period);
|
||||
double[] assets = [100, 101, 99, 102, 98, 103, 97, 104];
|
||||
double[] markets = [100, 100.5, 99.5, 101, 99, 101.5, 98.5, 102];
|
||||
|
||||
for (int i = 0; i < assets.Length - 1; i++)
|
||||
{
|
||||
cA.Update(assets[i], markets[i]);
|
||||
}
|
||||
// Feed last bar, then correct it
|
||||
cA.Update(999.0, 999.0);
|
||||
cA.Update(assets[^1], markets[^1], isNew: false);
|
||||
|
||||
// Path B: feed all bars cleanly
|
||||
var cB = new Convexity(period);
|
||||
for (int i = 0; i < assets.Length; i++)
|
||||
{
|
||||
cB.Update(assets[i], markets[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(cB.BetaStd, cA.BetaStd, 6);
|
||||
Assert.Equal(cB.ConvexityValue, cA.ConvexityValue, 6);
|
||||
Assert.Equal(cB.BetaUp, cA.BetaUp, 6);
|
||||
Assert.Equal(cB.BetaDown, cA.BetaDown, 6);
|
||||
}
|
||||
|
||||
// ── H. Batch API ─────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Batch_MatchesStreaming()
|
||||
{
|
||||
const int period = 5;
|
||||
var assetSeries = new TSeries(10);
|
||||
var marketSeries = new TSeries(10);
|
||||
var rng = new Random(42);
|
||||
|
||||
double ast = 100, mkt = 100;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double sign = (i % 2 == 0) ? 1 : -1;
|
||||
double magnitude = 0.005 + rng.NextDouble() * 0.02;
|
||||
ast *= (1 + sign * magnitude * 1.5);
|
||||
mkt *= (1 + sign * magnitude);
|
||||
assetSeries.Add(new TValue(i, ast));
|
||||
marketSeries.Add(new TValue(i, mkt));
|
||||
}
|
||||
|
||||
var (betaStdS, _, _, _, convexityS) = Convexity.Batch(assetSeries, marketSeries, period);
|
||||
|
||||
// Compare last value with streaming
|
||||
var streaming = new Convexity(period);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
streaming.Update(assetSeries[i], marketSeries[i]);
|
||||
}
|
||||
|
||||
Assert.Equal(streaming.BetaStd, betaStdS[^1].Value, 8);
|
||||
Assert.Equal(streaming.ConvexityValue, convexityS[^1].Value, 8);
|
||||
Assert.Equal(10, convexityS.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_Throws()
|
||||
{
|
||||
var a = new TSeries(5);
|
||||
var b = new TSeries(3);
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
a.Add(new TValue(i, 100 + i));
|
||||
}
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
b.Add(new TValue(i, 100 + i));
|
||||
}
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Convexity.Batch(a, b, 5));
|
||||
}
|
||||
|
||||
// ── I. Double overload ───────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void DoubleOverload_ProducesFiniteResults()
|
||||
{
|
||||
var c = new Convexity(5);
|
||||
c.Update(100.0, 100.0);
|
||||
c.Update(101.0, 101.0);
|
||||
c.Update(99.0, 99.5);
|
||||
c.Update(102.0, 101.5);
|
||||
c.Update(98.0, 99.0);
|
||||
c.Update(103.0, 102.0);
|
||||
|
||||
Assert.True(double.IsFinite(c.ConvexityValue));
|
||||
Assert.True(double.IsFinite(c.BetaStd));
|
||||
Assert.True(double.IsFinite(c.BetaUp));
|
||||
Assert.True(double.IsFinite(c.BetaDown));
|
||||
}
|
||||
|
||||
// ── J. Edge cases ────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void ConstantPrices_BetaIsZero()
|
||||
{
|
||||
var c = new Convexity(5);
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
c.Update(100.0, 100.0);
|
||||
}
|
||||
|
||||
// Constant prices → zero returns → zero variance → beta = 0
|
||||
Assert.Equal(0, c.BetaStd);
|
||||
Assert.Equal(0, c.ConvexityValue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroPrevPrice_ReturnsZero()
|
||||
{
|
||||
var c = new Convexity(5);
|
||||
c.Update(0.0, 0.0);
|
||||
c.Update(100.0, 100.0);
|
||||
|
||||
// Division by zero for return computation should be handled
|
||||
Assert.True(double.IsFinite(c.ConvexityValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativeBeta_HandledCorrectly()
|
||||
{
|
||||
// Asset moves opposite to market → negative beta
|
||||
const int period = 10;
|
||||
var c = new Convexity(period);
|
||||
var rng = new Random(42);
|
||||
|
||||
double mkt = 100, ast = 100;
|
||||
c.Update(ast, mkt);
|
||||
|
||||
for (int i = 1; i <= period; i++)
|
||||
{
|
||||
double sign = (i % 2 == 0) ? 1 : -1;
|
||||
double magnitude = 0.01 + rng.NextDouble() * 0.03;
|
||||
double mktRet = sign * magnitude;
|
||||
mkt *= (1 + mktRet);
|
||||
ast *= (1 - mktRet); // inverse
|
||||
c.Update(ast, mkt);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot);
|
||||
Assert.True(c.BetaStd < 0, $"BetaStd={c.BetaStd} should be negative for inverse relationship");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ratio_DivisionByZero_ReturnsZero()
|
||||
{
|
||||
// If all market bars go up, BetaDown = 0, Ratio should be 0
|
||||
const int period = 5;
|
||||
var c = new Convexity(period);
|
||||
|
||||
double mkt = 100, ast = 100;
|
||||
c.Update(ast, mkt);
|
||||
|
||||
for (int i = 1; i <= period; i++)
|
||||
{
|
||||
mkt *= 1.01; // always up
|
||||
ast *= 1.02;
|
||||
c.Update(ast, mkt);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot);
|
||||
Assert.Equal(0, c.BetaDown);
|
||||
Assert.Equal(0, c.Ratio);
|
||||
}
|
||||
|
||||
// ── K. Streaming consistency ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void LongStream_RemainsFinite()
|
||||
{
|
||||
var c = new Convexity(20);
|
||||
var rng = new Random(123);
|
||||
|
||||
double ast = 100, mkt = 100;
|
||||
c.Update(ast, mkt);
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
ast *= (1 + (rng.NextDouble() - 0.5) * 0.04);
|
||||
mkt *= (1 + (rng.NextDouble() - 0.5) * 0.02);
|
||||
c.Update(ast, mkt);
|
||||
|
||||
Assert.True(double.IsFinite(c.ConvexityValue), $"ConvexityValue not finite at i={i}");
|
||||
Assert.True(double.IsFinite(c.BetaStd), $"BetaStd not finite at i={i}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GBM_ProducesReasonableValues()
|
||||
{
|
||||
var gbmAsset = new GBM(100.0, 0.05, 0.3, seed: 42);
|
||||
var gbmMarket = new GBM(100.0, 0.04, 0.15, seed: 99);
|
||||
|
||||
var assetBars = gbmAsset.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromDays(1));
|
||||
var marketBars = gbmMarket.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromDays(1));
|
||||
|
||||
var c = new Convexity(20);
|
||||
for (int i = 0; i < assetBars.Count; i++)
|
||||
{
|
||||
c.Update(assetBars[i].Close, marketBars[i].Close);
|
||||
}
|
||||
|
||||
Assert.True(c.IsHot);
|
||||
Assert.True(double.IsFinite(c.ConvexityValue));
|
||||
Assert.True(double.IsFinite(c.BetaStd));
|
||||
Assert.True(c.ConvexityValue >= 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user