diff --git a/README.md b/README.md
index 88d41661..139a2ae4 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,8 @@ The following list of indicators are currently supported by this package:
- [Super Trend](volatility/README.md#SuperTrend)
- [True Range (TR)](volatility/README.md#TrueRange)
- [Ulcer Index (UI)](volatility/README.md#UlcerIndex)
+- [Z-Score](volatility/README.md#ZScore)
+
### 📢 Volume Indicators
diff --git a/volatility/README.md b/volatility/README.md
index 927a7f49..453a2295 100644
--- a/volatility/README.md
+++ b/volatility/README.md
@@ -123,6 +123,13 @@ The information provided on this project is strictly for informational purposes
- [func \(u \*UlcerIndex\[T\]\) Compute\(closings \<\-chan T\) \<\-chan T](<#UlcerIndex[T].Compute>)
- [func \(u \*UlcerIndex\[T\]\) ComputeWithContext\(ctx context.Context, closings \<\-chan T\) \<\-chan T](<#UlcerIndex[T].ComputeWithContext>)
- [func \(u \*UlcerIndex\[T\]\) IdlePeriod\(\) int](<#UlcerIndex[T].IdlePeriod>)
+- [type ZScore](<#ZScore>)
+ - [func NewZScore\[T helper.Number\]\(\) \*ZScore\[T\]](<#NewZScore>)
+ - [func NewZScoreWithPeriod\[T helper.Number\]\(period int\) \*ZScore\[T\]](<#NewZScoreWithPeriod>)
+ - [func \(z \*ZScore\[T\]\) Compute\(c \<\-chan T\) \<\-chan T](<#ZScore[T].Compute>)
+ - [func \(z \*ZScore\[T\]\) ComputeWithContext\(ctx context.Context, c \<\-chan T\) \<\-chan T](<#ZScore[T].ComputeWithContext>)
+ - [func \(z \*ZScore\[T\]\) IdlePeriod\(\) int](<#ZScore[T].IdlePeriod>)
+ - [func \(z \*ZScore\[T\]\) String\(\) string](<#ZScore[T].String>)
## Constants
@@ -254,6 +261,15 @@ const (
)
```
+
+
+```go
+const (
+ // DefaultZScorePeriod is the default period for Z-Score.
+ DefaultZScorePeriod = 20
+)
+```
+
## type [AccelerationBands]()
@@ -1442,4 +1458,83 @@ func (u *UlcerIndex[T]) IdlePeriod() int
IdlePeriod is the initial period that Ulcer Index won't yield any results.
+
+## type [ZScore]()
+
+ZScore represents the configuration parameters for Z\-Score. It measures how many standard deviations price is away from its SMA.
+
+```
+Z-Score = (Price - SMA) / StdDev
+```
+
+Example:
+
+```
+z := NewZScore[float64]()
+z.Compute(c)
+```
+
+```go
+type ZScore[T helper.Number] struct {
+ // Period is the time period.
+ Period int
+}
+```
+
+
+### func [NewZScore]()
+
+```go
+func NewZScore[T helper.Number]() *ZScore[T]
+```
+
+NewZScore function initializes a new Z\-Score instance with default parameters.
+
+
+### func [NewZScoreWithPeriod]()
+
+```go
+func NewZScoreWithPeriod[T helper.Number](period int) *ZScore[T]
+```
+
+NewZScoreWithPeriod function initializes a new Z\-Score instance with the given period.
+
+
+### func \(\*ZScore\[T\]\) [Compute]()
+
+```go
+func (z *ZScore[T]) Compute(c <-chan T) <-chan T
+```
+
+Compute wraps ComputeWithContext for backwards compatibility.
+
+Deprecated: Use ComputeWithContext instead.
+
+
+### func \(\*ZScore\[T\]\) [ComputeWithContext]()
+
+```go
+func (z *ZScore[T]) ComputeWithContext(ctx context.Context, c <-chan T) <-chan T
+```
+
+ComputeWithContext function takes a channel of numbers and computes the Z\-Score over the specified period.
+
+
+### func \(\*ZScore\[T\]\) [IdlePeriod]()
+
+```go
+func (z *ZScore[T]) IdlePeriod() int
+```
+
+IdlePeriod is the initial period that Z\-Score won't yield any results.
+
+
+### func \(\*ZScore\[T\]\) [String]()
+
+```go
+func (z *ZScore[T]) String() string
+```
+
+String is the string representation of Z\-Score.
+
Generated by [gomarkdoc]()
diff --git a/volatility/testdata/z_score.csv b/volatility/testdata/z_score.csv
new file mode 100644
index 00000000..4ab5c4eb
--- /dev/null
+++ b/volatility/testdata/z_score.csv
@@ -0,0 +1,252 @@
+Close,Expected
+318.600006,0
+315.839996,0
+316.149994,0
+310.570007,0
+307.779999,0
+305.820007,0
+305.98999,0
+306.390015,0
+311.450012,0
+312.329987,0
+309.290009,0
+301.910004,0
+300,0
+300.029999,0
+302,0
+307.820007,0
+302.690002,0
+306.48999,0
+305.549988,0
+303.429993,-0.7818245201783479
+309.059998,0.44386708800174934
+308.899994,0.5362477750787341
+309.910004,0.9801095220341718
+314.549988,2.0293027206369354
+312.899994,1.4591655753270345
+318.690002,2.2950636380966167
+315.529999,1.4645329688392388
+316.350006,1.443948525777569
+320.369995,1.90176400896643
+318.929993,1.52055333123116
+317.640015,1.2043857982759751
+314.859985,0.7050311757089405
+308.299988,-0.395395666875024
+305.230011,-1.0038777863570185
+309.869995,-0.2780314866723805
+310.420013,-0.20143900697090736
+311.299988,-0.12580001787849465
+311.899994,-0.060276113847900874
+310.950012,-0.3357354989863008
+309.170013,-0.8801522599853688
+307.329987,-1.2823035787971355
+311.519989,-0.31085063693906695
+310.570007,-0.5550448446819526
+311.859985,-0.20424742360541354
+308.51001,-0.9559455586073401
+308.429993,-0.8880195803926622
+312.970001,0.29466575663229827
+308.480011,-0.7746868070319617
+307.209991,-1.075725288726656
+309.890015,-0.15745429282962412
+313.73999,1.5637350055610275
+310.790009,0.42340951815663813
+309.630005,-0.17767840551442882
+308.179993,-1.114429910626945
+308.23999,-1.0064837823597135
+302.720001,-2.889360559532872
+303.160004,-2.2128650229311475
+303.070007,-1.9191622035206075
+304.019989,-1.4239950234198222
+304.660004,-1.1108734717334017
+305.179993,-0.8988661591869134
+304.619995,-0.9672312780422148
+307.75,0.029330140836829555
+312.450012,1.4604186400923211
+316.970001,2.3079947121635165
+311.119995,0.7387034594312188
+311.369995,0.8412046209959448
+304.820007,-0.8141809410368722
+303.630005,-1.0444812963968493
+302.880005,-1.1145482604444217
+305.329987,-0.44054476244466695
+297.880005,-2.007526040565908
+302.01001,-0.9352321248395642
+293.51001,-2.3398771011768384
+301.059998,-0.7614173258388136
+303.850006,-0.22169160841011898
+299.730011,-0.9827993840695142
+298.369995,-1.1611070757544648
+298.920013,-0.9842919321503065
+302.140015,-0.37143714784381743
+302.320007,-0.3120836006550457
+305.299988,0.22331049490758859
+305.079987,0.20983980503871374
+308.769989,0.9623244219319945
+310.309998,1.5223411861481093
+309.070007,1.3160804620303015
+310.390015,1.6619470273629715
+312.51001,1.872904315879046
+312.619995,1.6646040970667328
+313.700012,1.6420270228377845
+314.549988,1.5945075988426614
+318.049988,1.8911061123196824
+319.73999,1.8485719698604104
+323.790009,2.155983766168164
+324.630005,1.944323022457516
+323.089996,1.538122717286768
+323.820007,1.46549475280849
+324.329987,1.4009070794485172
+326.049988,1.490002285238737
+324.339996,1.165952669774801
+320.529999,0.580354171268126
+326.230011,1.2935315372684537
+328.549988,1.51988422606345
+330.170013,1.5977665676831478
+325.859985,0.8461826951582786
+323.220001,0.33886416450274265
+320,-0.35279777612996377
+323.880005,0.3298564831460812
+326.140015,0.7574208979046654
+324.869995,0.36406143935435065
+322.98999,-0.36035732647833446
+322.640015,-0.6369151334031727
+322.48999,-0.8093543846174371
+323.529999,-0.35805634710340467
+323.75,-0.24494852680859133
+327.390015,1.1798668450651713
+329.76001,1.8495354402438706
+330.390015,1.7983809721655972
+329.130005,1.261138399054557
+323.109985,-0.6902814774401368
+320.200012,-1.618062650914307
+319.019989,-1.732324672885001
+320.600006,-1.1419535718969562
+322.190002,-0.5944815755688119
+321.079987,-0.8619572745473221
+323.119995,-0.21833125174547793
+329.480011,1.583016288116815
+328.579987,1.1904164755612572
+333.410004,2.1790190708258557
+335.420013,2.2061799721934268
+335.950012,1.9616998653150342
+335.290009,1.6050476689837074
+333.600006,1.1618467680967748
+336.390015,1.4804268422218436
+335.899994,1.2521568251918307
+339.820007,1.6732061285700697
+338.309998,1.3074281295162973
+338.670013,1.2456964072585837
+338.609985,1.1297556943822218
+336.959991,0.8140382904338024
+335.25,0.4986792619872186
+334.119995,0.2436912425451558
+335.339996,0.36135182521815834
+334.149994,0.03700702058384454
+336.910004,0.5636116719499273
+341,1.7942997552105606
+342,1.9520306570644763
+341.559998,1.8041259013596094
+341.459991,1.579388515150803
+340.899994,1.2266007185571075
+341.130005,1.1821914723143536
+343.369995,1.735307763425763
+345.350006,2.0841599716675514
+343.540009,1.3429986940436196
+341.089996,0.5122109459767898
+344.25,1.3722741336593396
+345.339996,1.503941490540785
+342.429993,0.6193729478365246
+346.609985,1.5813243045170298
+345.76001,1.220986145995572
+349.630005,1.9493854518634202
+347.579987,1.3457467232072482
+349.799988,1.76326674804902
+349.309998,1.5979113261313147
+349.809998,1.6431322027952942
+351.959991,1.981376320665765
+352.26001,1.7899481572660285
+351.190002,1.346810331863959
+353.809998,1.7897129956655795
+349.98999,0.7392561304434329
+362.579987,2.984461576448763
+363.730011,2.5257387083525744
+358.019989,1.3617427684468164
+356.980011,1.0736293923623093
+358.350006,1.1983565118639121
+358.480011,1.1081419717266858
+354.5,0.3395985969543774
+354.109985,0.1788833347362968
+353.190002,-0.07667156012909344
+352.559998,-0.30406966574020183
+352.089996,-0.44842183607721453
+350.570007,-0.873617801261981
+354.26001,-0.03194514937519997
+354.299988,-0.08827134166517518
+355.929993,0.26961257667020694
+355.549988,0.11882655773343154
+358.290009,0.7966151439385621
+361.059998,1.4052411204731305
+360.200012,1.0597321197585208
+362.459991,1.519428627631854
+360.470001,1.0471723581359316
+361.670013,1.4894417408746745
+361.799988,1.4000210424396031
+363.149994,1.5790573521890885
+365.519989,1.9020695459568842
+367.779999,2.0558677240065837
+367.820007,1.7815680693701117
+369.5,1.8172665759699478
+367.859985,1.3657046749032746
+370.429993,1.621754764610057
+370.480011,1.4802097796729616
+366.820007,0.7768587033448849
+363.279999,0.012712881696167005
+360.160004,-0.7530413821510269
+361.709991,-0.5068328260899367
+359.420013,-1.1998984763055782
+357.779999,-1.606755512964339
+357.059998,-1.6387641165774691
+350.299988,-2.597500895387828
+348.079987,-2.4144904403810443
+343.040009,-2.536936953405212
+343.690002,-2.0522109114156963
+345.059998,-1.6541984430407575
+346.339996,-1.353021746175078
+345.450012,-1.2961860845088597
+348.559998,-0.8840445468388387
+348.429993,-0.8098898000026656
+345.660004,-1.0014728621229647
+345.089996,-0.9669857028085983
+346.230011,-0.7674233052340442
+345.390015,-0.8094665754914792
+340.890015,-1.3462692885841976
+338.660004,-1.5556992439592987
+335.859985,-1.785961755769725
+336.839996,-1.5526500247535628
+338.630005,-1.2017304698478237
+336.899994,-1.4515056525712233
+336.160004,-1.5825815449999596
+331.709991,-2.192493067415746
+337.410004,-0.9203079766352411
+341.329987,-0.08073831112706527
+343.75,0.42666149875115156
+349.019989,1.427721227918211
+351.809998,1.7989285949347475
+346.630005,0.8129615733078429
+346.170013,0.7662857057490063
+346.299988,0.8280660407970325
+348.179993,1.1418077989641282
+350.559998,1.4631585272910645
+350.01001,1.2869729428696268
+354.25,1.7871869672584084
+356.790009,1.8715137181789667
+359.859985,1.960424111266184
+358.929993,1.6267539386786765
+361.329987,1.6970984810567131
+361,1.4753381459646424
+361.799988,1.4154345584816068
+362.679993,1.3859317660637405
+361.339996,1.187690392272072
+360.049988,0.9503603912423864
+358.690002,0.6756284675797073
diff --git a/volatility/z_score.go b/volatility/z_score.go
new file mode 100644
index 00000000..e6a641bf
--- /dev/null
+++ b/volatility/z_score.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 (
+ "context"
+ "fmt"
+
+ "github.com/cinar/indicator/v2/helper"
+ "github.com/cinar/indicator/v2/trend"
+)
+
+const (
+ // DefaultZScorePeriod is the default period for Z-Score.
+ DefaultZScorePeriod = 20
+)
+
+// ZScore represents the configuration parameters for Z-Score.
+// It measures how many standard deviations price is away from its SMA.
+//
+// Z-Score = (Price - SMA) / StdDev
+//
+// Example:
+//
+// z := NewZScore[float64]()
+// z.Compute(c)
+type ZScore[T helper.Number] struct {
+ // Period is the time period.
+ Period int
+}
+
+// NewZScore function initializes a new Z-Score instance with default parameters.
+func NewZScore[T helper.Number]() *ZScore[T] {
+ return NewZScoreWithPeriod[T](DefaultZScorePeriod)
+}
+
+// NewZScoreWithPeriod function initializes a new Z-Score instance with the given period.
+func NewZScoreWithPeriod[T helper.Number](period int) *ZScore[T] {
+ return &ZScore[T]{
+ Period: period,
+ }
+}
+
+// ComputeWithContext function takes a channel of numbers and computes the Z-Score over the specified period.
+func (z *ZScore[T]) ComputeWithContext(ctx context.Context, c <-chan T) <-chan T {
+ cs := helper.DuplicateWithContext(ctx, c, 3)
+
+ sma := trend.NewSmaWithPeriod[T](z.Period)
+ std := NewMovingStdWithPeriod[T](z.Period)
+
+ smaChan := sma.ComputeWithContext(ctx, cs[0])
+ stdChan := std.ComputeWithContext(ctx, cs[1])
+ priceChan := helper.SkipWithContext(ctx, cs[2], z.IdlePeriod())
+
+ return helper.DivideWithContext(ctx, helper.SubtractWithContext(ctx, priceChan, smaChan), stdChan)
+}
+
+// IdlePeriod is the initial period that Z-Score won't yield any results.
+func (z *ZScore[T]) IdlePeriod() int {
+ return z.Period - 1
+}
+
+// String is the string representation of Z-Score.
+func (z *ZScore[T]) String() string {
+ return fmt.Sprintf("ZSCORE(%d)", z.Period)
+}
+
+// Compute wraps ComputeWithContext for backwards compatibility.
+//
+// Deprecated: Use ComputeWithContext instead.
+func (z *ZScore[T]) Compute(c <-chan T) <-chan T {
+ return z.ComputeWithContext(context.Background(), c)
+}
diff --git a/volatility/z_score_test.go b/volatility/z_score_test.go
new file mode 100644
index 00000000..978abbd1
--- /dev/null
+++ b/volatility/z_score_test.go
@@ -0,0 +1,58 @@
+// 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 TestZScore(t *testing.T) {
+ type Data struct {
+ Close float64 `header:"Close"`
+ Expected float64 `header:"Expected"`
+ }
+
+ input, err := helper.ReadFromCsvFile[Data]("testdata/z_score.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.Expected })
+
+ z := volatility.NewZScore[float64]()
+ actual := z.Compute(closings)
+ actual = helper.RoundDigits(actual, 2)
+
+ expected = helper.RoundDigits(expected, 2)
+ expected = helper.Skip(expected, z.IdlePeriod())
+
+ err = helper.CheckEquals(actual, expected)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestZScoreString(t *testing.T) {
+ z := volatility.NewZScoreWithPeriod[float64](14)
+ expected := "ZSCORE(14)"
+
+ if z.String() != expected {
+ t.Fatalf("expected %s actual %s", expected, z.String())
+ }
+}
+
+func TestNewZScore(t *testing.T) {
+ z := volatility.NewZScore[float64]()
+ expectedPeriod := 20
+
+ if z.Period != expectedPeriod {
+ t.Fatalf("expected period %d actual %d", expectedPeriod, z.Period)
+ }
+}