From 78fdb7cb05d3cde9c6eed18b1bc8a7e52aef76ad Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Thu, 28 May 2026 18:28:17 -0700 Subject: [PATCH] Implement Annualized Historical Volatility (AHV) indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the AHV indicator to the volatility package. AHV annualizes Historical Volatility by multiplying it by the square root of trading days per year (default 252). AHV = HV × √252 - Struct: AnnualizedHistoricalVolatility[T helper.Number] - Constructors: NewAnnualizedHistoricalVolatility, NewAnnualizedHistoricalVolatilityWithPeriod - Composes HistoricalVolatility with helper.MultiplyBy - 100% test coverage with CSV-based validation - Updated root and volatility README indexes Closes #368 --- README.md | 1 + volatility/README.md | 95 +++++++ .../annualized_historical_volatility.go | 75 ++++++ .../annualized_historical_volatility_test.go | 67 +++++ .../annualized_historical_volatility.csv | 252 ++++++++++++++++++ 5 files changed, 490 insertions(+) create mode 100644 volatility/annualized_historical_volatility.go create mode 100644 volatility/annualized_historical_volatility_test.go create mode 100644 volatility/testdata/annualized_historical_volatility.csv diff --git a/README.md b/README.md index 709b1e5..dc39254 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ The following list of indicators are currently supported by this package: - [Percent B](volatility/README.md#PercentB) - [Acceleration Bands](volatility/README.md#AccelerationBands) +- [Annualized Historical Volatility (AHV)](volatility/README.md#AnnualizedHistoricalVolatility) - [Average True Range (ATR)](volatility/README.md#Atr) - [Bollinger Band Width](volatility/README.md#BollingerBandWidth) - [Bollinger Bands](volatility/README.md#BollingerBands) diff --git a/volatility/README.md b/volatility/README.md index 836df58..927a7f4 100644 --- a/volatility/README.md +++ b/volatility/README.md @@ -30,6 +30,13 @@ The information provided on this project is strictly for informational purposes - [func \(a \*AccelerationBands\[T\]\) Compute\(high, low, closing \<\-chan T\) \(\<\-chan T, \<\-chan T, \<\-chan T\)](<#AccelerationBands[T].Compute>) - [func \(a \*AccelerationBands\[T\]\) ComputeWithContext\(ctx context.Context, high, low, closing \<\-chan T\) \(\<\-chan T, \<\-chan T, \<\-chan T\)](<#AccelerationBands[T].ComputeWithContext>) - [func \(a \*AccelerationBands\[T\]\) IdlePeriod\(\) int](<#AccelerationBands[T].IdlePeriod>) +- [type AnnualizedHistoricalVolatility](<#AnnualizedHistoricalVolatility>) + - [func NewAnnualizedHistoricalVolatility\[T helper.Number\]\(\) \*AnnualizedHistoricalVolatility\[T\]](<#NewAnnualizedHistoricalVolatility>) + - [func NewAnnualizedHistoricalVolatilityWithPeriod\[T helper.Number\]\(period int\) \*AnnualizedHistoricalVolatility\[T\]](<#NewAnnualizedHistoricalVolatilityWithPeriod>) + - [func \(a \*AnnualizedHistoricalVolatility\[T\]\) Compute\(prices \<\-chan T\) \<\-chan T](<#AnnualizedHistoricalVolatility[T].Compute>) + - [func \(a \*AnnualizedHistoricalVolatility\[T\]\) ComputeWithContext\(ctx context.Context, prices \<\-chan T\) \<\-chan T](<#AnnualizedHistoricalVolatility[T].ComputeWithContext>) + - [func \(a \*AnnualizedHistoricalVolatility\[T\]\) IdlePeriod\(\) int](<#AnnualizedHistoricalVolatility[T].IdlePeriod>) + - [func \(a \*AnnualizedHistoricalVolatility\[T\]\) String\(\) string](<#AnnualizedHistoricalVolatility[T].String>) - [type Atr](<#Atr>) - [func NewAtr\[T helper.Number\]\(\) \*Atr\[T\]](<#NewAtr>) - [func NewAtrWithMa\[T helper.Number\]\(ma trend.Ma\[T\]\) \*Atr\[T\]](<#NewAtrWithMa>) @@ -120,6 +127,19 @@ The information provided on this project is strictly for informational purposes ## Constants + + +```go +const ( + // DefaultAnnualizedHistoricalVolatilityPeriod is the default period for + // Annualized Historical Volatility (AHV). + DefaultAnnualizedHistoricalVolatilityPeriod = 21 + + // DefaultTradingDaysPerYear is the standard number of trading days in a year. + DefaultTradingDaysPerYear = 252 +) +``` + ```go @@ -297,6 +317,81 @@ func (a *AccelerationBands[T]) IdlePeriod() int IdlePeriod is the initial period that Acceleration Bands won't yield any results. + +## type [AnnualizedHistoricalVolatility]() + +AnnualizedHistoricalVolatility represents the configuration parameters for calculating Annualized Historical Volatility \(AHV\). It annualizes the Historical Volatility by multiplying it by the square root of the number of trading days per year. + +``` +AHV = HV × √TradingDaysPerYear +``` + +```go +type AnnualizedHistoricalVolatility[T helper.Number] struct { + // Hv is the underlying Historical Volatility indicator. + Hv *HistoricalVolatility[T] + + // TradingDaysPerYear is the number of trading days in a year (default: 252). + TradingDaysPerYear int +} +``` + + +### func [NewAnnualizedHistoricalVolatility]() + +```go +func NewAnnualizedHistoricalVolatility[T helper.Number]() *AnnualizedHistoricalVolatility[T] +``` + +NewAnnualizedHistoricalVolatility function initializes a new Annualized Historical Volatility instance with the default parameters. + + +### func [NewAnnualizedHistoricalVolatilityWithPeriod]() + +```go +func NewAnnualizedHistoricalVolatilityWithPeriod[T helper.Number](period int) *AnnualizedHistoricalVolatility[T] +``` + +NewAnnualizedHistoricalVolatilityWithPeriod function initializes a new Annualized Historical Volatility instance with the given period. + + +### func \(\*AnnualizedHistoricalVolatility\[T\]\) [Compute]() + +```go +func (a *AnnualizedHistoricalVolatility[T]) Compute(prices <-chan T) <-chan T +``` + +Compute wraps ComputeWithContext for backwards compatibility. + +Deprecated: Use ComputeWithContext instead. + + +### func \(\*AnnualizedHistoricalVolatility\[T\]\) [ComputeWithContext]() + +```go +func (a *AnnualizedHistoricalVolatility[T]) ComputeWithContext(ctx context.Context, prices <-chan T) <-chan T +``` + +ComputeWithContext function takes a channel of prices and computes the Annualized Historical Volatility over the specified period. + + +### func \(\*AnnualizedHistoricalVolatility\[T\]\) [IdlePeriod]() + +```go +func (a *AnnualizedHistoricalVolatility[T]) IdlePeriod() int +``` + +IdlePeriod is the initial period that Annualized Historical Volatility won't yield any results. + + +### func \(\*AnnualizedHistoricalVolatility\[T\]\) [String]() + +```go +func (a *AnnualizedHistoricalVolatility[T]) String() string +``` + +String function returns a string representation of the Annualized Historical Volatility. + ## type [Atr]() diff --git a/volatility/annualized_historical_volatility.go b/volatility/annualized_historical_volatility.go new file mode 100644 index 0000000..f74fbd5 --- /dev/null +++ b/volatility/annualized_historical_volatility.go @@ -0,0 +1,75 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package volatility + +import ( + "fmt" + "math" + + "context" + + "github.com/cinar/indicator/v2/helper" +) + +const ( + // DefaultAnnualizedHistoricalVolatilityPeriod is the default period for + // Annualized Historical Volatility (AHV). + DefaultAnnualizedHistoricalVolatilityPeriod = 21 + + // DefaultTradingDaysPerYear is the standard number of trading days in a year. + DefaultTradingDaysPerYear = 252 +) + +// AnnualizedHistoricalVolatility represents the configuration parameters for calculating +// Annualized Historical Volatility (AHV). It annualizes the Historical Volatility by +// multiplying it by the square root of the number of trading days per year. +// +// AHV = HV × √TradingDaysPerYear +type AnnualizedHistoricalVolatility[T helper.Number] struct { + // Hv is the underlying Historical Volatility indicator. + Hv *HistoricalVolatility[T] + + // TradingDaysPerYear is the number of trading days in a year (default: 252). + TradingDaysPerYear int +} + +// NewAnnualizedHistoricalVolatility function initializes a new Annualized Historical Volatility +// instance with the default parameters. +func NewAnnualizedHistoricalVolatility[T helper.Number]() *AnnualizedHistoricalVolatility[T] { + return NewAnnualizedHistoricalVolatilityWithPeriod[T](DefaultAnnualizedHistoricalVolatilityPeriod) +} + +// NewAnnualizedHistoricalVolatilityWithPeriod function initializes a new Annualized Historical +// Volatility instance with the given period. +func NewAnnualizedHistoricalVolatilityWithPeriod[T helper.Number](period int) *AnnualizedHistoricalVolatility[T] { + return &AnnualizedHistoricalVolatility[T]{ + Hv: NewHistoricalVolatilityWithPeriod[T](period), + TradingDaysPerYear: DefaultTradingDaysPerYear, + } +} + +// ComputeWithContext function takes a channel of prices and computes the Annualized Historical +// Volatility over the specified period. +func (a *AnnualizedHistoricalVolatility[T]) ComputeWithContext(ctx context.Context, prices <-chan T) <-chan T { + hv := a.Hv.ComputeWithContext(ctx, prices) + return helper.MultiplyByWithContext(ctx, hv, T(math.Sqrt(float64(a.TradingDaysPerYear)))) +} + +// IdlePeriod is the initial period that Annualized Historical Volatility won't yield any results. +func (a *AnnualizedHistoricalVolatility[T]) IdlePeriod() int { + return a.Hv.IdlePeriod() +} + +// String function returns a string representation of the Annualized Historical Volatility. +func (a *AnnualizedHistoricalVolatility[T]) String() string { + return fmt.Sprintf("AHV(%d)", a.Hv.Period) +} + +// Compute wraps ComputeWithContext for backwards compatibility. +// +// Deprecated: Use ComputeWithContext instead. +func (a *AnnualizedHistoricalVolatility[T]) Compute(prices <-chan T) <-chan T { + return a.ComputeWithContext(context.Background(), prices) +} diff --git a/volatility/annualized_historical_volatility_test.go b/volatility/annualized_historical_volatility_test.go new file mode 100644 index 0000000..5e164d2 --- /dev/null +++ b/volatility/annualized_historical_volatility_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2021-2026 Onur Cinar. +// The source code is provided under GNU AGPLv3 License. +// https://github.com/cinar/indicator + +package volatility_test + +import ( + "testing" + + "github.com/cinar/indicator/v2/helper" + "github.com/cinar/indicator/v2/volatility" +) + +func TestAnnualizedHistoricalVolatilityDefaultAndString(t *testing.T) { + ahv := volatility.NewAnnualizedHistoricalVolatility[float64]() + + if ahv.Hv.Period != volatility.DefaultAnnualizedHistoricalVolatilityPeriod { + t.Fatalf("expected period %d, got %d", volatility.DefaultAnnualizedHistoricalVolatilityPeriod, ahv.Hv.Period) + } + + if ahv.TradingDaysPerYear != volatility.DefaultTradingDaysPerYear { + t.Fatalf("expected trading days %d, got %d", volatility.DefaultTradingDaysPerYear, ahv.TradingDaysPerYear) + } + + if ahv.IdlePeriod() != volatility.DefaultAnnualizedHistoricalVolatilityPeriod { + t.Fatalf("expected idle period %d, got %d", volatility.DefaultAnnualizedHistoricalVolatilityPeriod, ahv.IdlePeriod()) + } + + if ahv.String() != "AHV(21)" { + t.Fatalf("expected AHV(21), got %s", ahv.String()) + } +} + +func TestAnnualizedHistoricalVolatilityWithInvalidPeriod(t *testing.T) { + ahv := volatility.NewAnnualizedHistoricalVolatilityWithPeriod[float64](0) + + if ahv.Hv.Period != volatility.DefaultAnnualizedHistoricalVolatilityPeriod { + t.Fatalf("expected default period %d, got %d", volatility.DefaultAnnualizedHistoricalVolatilityPeriod, ahv.Hv.Period) + } +} + +func TestAnnualizedHistoricalVolatility(t *testing.T) { + type Data struct { + Close float64 `header:"Close"` + Ahv float64 `header:"Ahv"` + } + + input, err := helper.ReadFromCsvFile[Data]("testdata/annualized_historical_volatility.csv") + if err != nil { + t.Fatal(err) + } + + inputs := helper.Duplicate(input, 2) + closings := helper.Map(inputs[0], func(d *Data) float64 { return d.Close }) + expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Ahv }) + + ahv := volatility.NewAnnualizedHistoricalVolatility[float64]() + actual := ahv.Compute(closings) + actual = helper.RoundDigits(actual, 8) + + expected = helper.Skip(expected, ahv.IdlePeriod()) + + err = helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} diff --git a/volatility/testdata/annualized_historical_volatility.csv b/volatility/testdata/annualized_historical_volatility.csv new file mode 100644 index 0000000..0a9b20b --- /dev/null +++ b/volatility/testdata/annualized_historical_volatility.csv @@ -0,0 +1,252 @@ +Close,Ahv +318.600006,0.00000000 +315.839996,0.00000000 +316.149994,0.00000000 +310.570007,0.00000000 +307.779999,0.00000000 +305.820007,0.00000000 +305.989990,0.00000000 +306.390015,0.00000000 +311.450012,0.00000000 +312.329987,0.00000000 +309.290009,0.00000000 +301.910004,0.00000000 +300.000000,0.00000000 +300.029999,0.00000000 +302.000000,0.00000000 +307.820007,0.00000000 +302.690002,0.00000000 +306.489990,0.00000000 +305.549988,0.00000000 +303.429993,0.00000000 +309.059998,0.00000000 +308.899994,0.18020067 +309.910004,0.17894562 +314.549988,0.18673414 +312.899994,0.17727106 +318.690002,0.18400484 +315.529999,0.18625476 +316.350006,0.18625086 +320.369995,0.18994569 +318.929993,0.18413371 +317.640015,0.18486835 +314.859985,0.18422167 +308.299988,0.17956196 +305.230011,0.18176109 +309.869995,0.18803613 +310.420013,0.18721595 +311.299988,0.17629364 +311.899994,0.16530925 +310.950012,0.16113094 +309.170013,0.16213162 +307.329987,0.16161359 +311.519989,0.15582780 +310.570007,0.15624623 +311.859985,0.15647588 +308.510010,0.15170988 +308.429993,0.15092142 +312.970001,0.14560940 +308.480011,0.14964517 +307.209991,0.14941245 +309.890015,0.14546949 +313.739990,0.15242393 +310.790009,0.15487657 +309.630005,0.15277302 +308.179993,0.13614627 +308.239990,0.13146503 +302.720001,0.13464974 +303.160004,0.13457245 +303.070007,0.13389105 +304.019989,0.13429525 +304.660004,0.13456609 +305.179993,0.13373583 +304.619995,0.13247088 +307.750000,0.12863653 +312.450012,0.13886408 +316.970001,0.14644486 +311.119995,0.15577594 +311.369995,0.15575935 +304.820007,0.16330852 +303.630005,0.15691340 +302.880005,0.15658212 +305.329987,0.15614131 +297.880005,0.16827517 +302.010010,0.17492611 +293.510010,0.19706276 +301.059998,0.21858941 +303.850006,0.22132764 +299.730011,0.21770177 +298.369995,0.21803874 +298.920013,0.21820191 +302.140015,0.22126053 +302.320007,0.22113081 +305.299988,0.22370651 +305.079987,0.22362145 +308.769989,0.22469036 +310.309998,0.21906318 +309.070007,0.21300616 +310.390015,0.20448401 +312.510010,0.20578905 +312.619995,0.19143241 +313.700012,0.19065738 +314.549988,0.19012492 +318.049988,0.19158842 +319.739990,0.16720250 +323.790009,0.16635249 +324.630005,0.12335897 +323.089996,0.10274343 +323.820007,0.10064242 +324.329987,0.08192428 +326.049988,0.07651641 +324.339996,0.08268905 +320.529999,0.09446161 +326.230011,0.10665490 +328.549988,0.10513161 +330.170013,0.10412540 +325.859985,0.11427526 +323.220001,0.11943652 +320.000000,0.12460474 +323.880005,0.12929959 +326.140015,0.12936597 +324.869995,0.13082998 +322.989990,0.13319228 +322.640015,0.13337074 +322.489990,0.12874110 +323.529999,0.12803325 +323.750000,0.12067087 +327.390015,0.12629379 +329.760010,0.12689291 +330.390015,0.12685700 +329.130005,0.12786226 +323.109985,0.14185341 +320.200012,0.14395221 +319.019989,0.13893365 +320.600006,0.12510216 +322.190002,0.12367291 +321.079987,0.12216651 +323.119995,0.11727721 +329.480011,0.13200049 +328.579987,0.12699707 +333.410004,0.12989749 +335.420013,0.12944854 +335.950012,0.12809288 +335.290009,0.12607253 +333.600006,0.12786036 +336.390015,0.12960057 +335.899994,0.13005459 +339.820007,0.13413949 +338.309998,0.13212011 +338.670013,0.13059681 +338.609985,0.13067119 +336.959991,0.13120727 +335.250000,0.11420968 +334.119995,0.10930448 +335.339996,0.10748215 +334.149994,0.10889367 +336.910004,0.11053055 +341.000000,0.11357236 +342.000000,0.11290943 +341.559998,0.09614275 +341.459991,0.09513147 +340.899994,0.08403827 +341.130005,0.08217774 +343.369995,0.08443099 +345.350006,0.08515519 +343.540009,0.08534043 +341.089996,0.08626954 +344.250000,0.09059465 +345.339996,0.08303101 +342.429993,0.08703384 +346.609985,0.09549992 +345.760010,0.09617866 +349.630005,0.09965100 +347.579987,0.10033519 +349.799988,0.09979052 +349.309998,0.10037229 +349.809998,0.09848346 +351.959991,0.09717397 +352.260010,0.09044122 +351.190002,0.09160197 +353.809998,0.09341569 +349.989990,0.10241569 +362.579987,0.15523041 +363.730011,0.15501335 +358.019989,0.16687035 +356.980011,0.16712515 +358.350006,0.16545379 +358.480011,0.16236558 +354.500000,0.16660997 +354.109985,0.16670766 +353.190002,0.16378700 +352.559998,0.15961564 +352.089996,0.15938156 +350.570007,0.15596663 +354.260010,0.15817713 +354.299988,0.15700981 +355.929993,0.15737627 +355.549988,0.15750990 +358.290009,0.15822922 +361.059998,0.15990523 +360.200012,0.15971069 +362.459991,0.15920740 +360.470001,0.15537333 +361.670013,0.09630825 +361.799988,0.09562822 +363.149994,0.07904701 +365.519989,0.08031965 +367.779999,0.08164555 +367.820007,0.08168322 +369.500000,0.06954939 +367.859985,0.07219182 +370.429993,0.07241448 +370.480011,0.07140507 +366.820007,0.08181473 +363.279999,0.08844832 +360.160004,0.08919486 +361.709991,0.08992806 +359.420013,0.09224259 +357.779999,0.09369251 +357.059998,0.09018739 +350.299988,0.10591062 +348.079987,0.10717951 +343.040009,0.11173634 +343.690002,0.11222728 +345.059998,0.11263997 +346.339996,0.11411700 +345.450012,0.11225836 +348.559998,0.11488987 +348.429993,0.11120524 +345.660004,0.11222923 +345.089996,0.10917210 +346.230011,0.11125727 +345.390015,0.10567458 +340.890015,0.10982118 +338.660004,0.10822030 +335.859985,0.10741717 +336.839996,0.10818303 +338.630005,0.10906984 +336.899994,0.10871456 +336.160004,0.10861443 +331.709991,0.11396157 +337.410004,0.12039883 +341.329987,0.12730139 +343.750000,0.12038702 +349.019989,0.13086714 +351.809998,0.13273809 +346.630005,0.14242668 +346.170013,0.14221105 +346.299988,0.13870613 +348.179993,0.14003559 +350.559998,0.13887284 +350.010010,0.13885677 +354.250000,0.14393114 +356.790009,0.14473805 +359.859985,0.13677750 +358.929993,0.13422734 +361.329987,0.12883205 +361.000000,0.12969498 +361.799988,0.12955115 +362.679993,0.12621993 +361.339996,0.12713661 +360.049988,0.11555733 +358.690002,0.10821885