blockchain/internal/lib/strategy/ema_test.go

171 lines
4.0 KiB
Go
Raw Normal View History

2025-08-15 01:36:36 +00:00
package strategy
import (
2025-08-17 14:48:13 +00:00
"sync"
2025-08-15 01:36:36 +00:00
"testing"
2025-08-17 14:48:13 +00:00
"time"
"github.com/shopspring/decimal"
2025-08-15 01:36:36 +00:00
)
2025-08-17 14:48:13 +00:00
func almostEqual(a, b decimal.Decimal, tol float64) bool {
if a.Equal(b) {
return true
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
diff := a.Sub(b).Abs()
return diff.LessThanOrEqual(decimal.NewFromFloat(tol))
}
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
// 產生參考 EMASMA-seed前 period 筆做 SMA 當種子,之後用 α 遞迴
func emaReferenceSMASeed(seq []float64, period uint) []decimal.Decimal {
if period == 0 {
panic("period must be > 0")
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
alpha := decimal.NewFromInt(2).
Div(decimal.NewFromInt(int64(period)).Add(decimal.NewFromInt(1)))
out := make([]decimal.Decimal, len(seq))
var sum decimal.Decimal
var ready bool
var emaVal decimal.Decimal
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
for i, x := range seq {
px := d(int64(x))
if !ready {
sum = sum.Add(px)
if uint(i+1) == period {
emaVal = sum.Div(decimal.NewFromInt(int64(period))) // SMA seed
ready = true
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
out[i] = emaVal
continue
}
emaVal = emaVal.Mul(decimal.NewFromInt(1).Sub(alpha)).Add(px.Mul(alpha))
out[i] = emaVal
}
return out
}
func TestEMA_WarmupAndStepByStep(t *testing.T) {
type step struct {
in float64
want float64
wantReady bool
}
alpha := 2.0 / (3.0 + 1.0) // period=3 => α=0.5
_ = alpha
// 序列1,2,3seed 完成 => 2.0),下一筆 4 => 0.5*4 + 0.5*2 = 3.0
steps := []step{
{in: 1, want: 0, wantReady: false}, // value 在未 ready 前不重要,但實作會回 seed 累積值;這步不檢查值
{in: 2, want: 0, wantReady: false},
{in: 3, want: 2.0, wantReady: true},
{in: 4, want: 3.0, wantReady: true},
}
e := NewEMA(3)
for i, st := range steps {
got := e.Update(d(int64(st.in)))
// 檢查 ready
if got.Ready != st.wantReady {
t.Fatalf("step %d: ready got=%v want=%v", i, got.Ready, st.wantReady)
}
// 檢查值(只有 ready 後才精準檢查)
if got.Ready {
if !almostEqual(got.Value, d(int64(st.want)), 1e-12) {
t.Fatalf("step %d: value got=%s want=%v", i, got.Value, st.want)
}
}
// Load() 與 Update() 一致
ld := e.Load()
if ld.Ready != got.Ready || !ld.Value.Equal(got.Value) {
t.Fatalf("step %d: Load() mismatch", i)
}
}
}
func TestEMA_MatchesReference_SMASeed(t *testing.T) {
period := uint(10)
seq := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14, 15, 16, 15, 14, 13, 12, 11, 10, 11, 12, 13}
ref := emaReferenceSMASeed(seq, period)
2025-08-15 01:36:36 +00:00
2025-08-17 14:48:13 +00:00
e := NewEMA(period)
for i, x := range seq {
out := e.Update(d(int64(x)))
// 只有在 ready 後才對齊 ref
if out.Ready {
if !almostEqual(out.Value, ref[i], 1e-9) {
t.Fatalf("i=%d: EMA value got=%s ref=%s", i, out.Value, ref[i])
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
} else {
// 未 ready 時,檢查準備進度是否合理
if uint(i+1) == period && !out.Ready {
t.Fatalf("i=%d: should be ready at the period boundary", i)
2025-08-15 01:36:36 +00:00
}
2025-08-17 14:48:13 +00:00
}
}
}
func TestEMA_PeriodOne_DegenerateCase(t *testing.T) {
// period=1 => α=1第一筆即 ready之後永遠等於最新價
e := NewEMA(1)
seq := []float64{10, 11, 12, 9, 8, 15}
for i, x := range seq {
out := e.Update(d(int64(x)))
if !out.Ready {
t.Fatalf("i=%d: period=1 should be ready immediately", i)
}
if !out.Value.Equal(d(int64(x))) {
t.Fatalf("i=%d: value got=%s want=%v", i, out.Value, x)
}
// Load() 同步
if !e.Load().Value.Equal(out.Value) {
t.Fatalf("i=%d: Load() mismatch", i)
}
}
}
func TestEMA_MultiReadersSingleWriter(t *testing.T) {
e := NewEMA(5)
var wg sync.WaitGroup
stop := make(chan struct{})
// 多 reader不斷 Load(),不應當出現資料競態或 panic
reader := func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = e.Load()
}
}
}
// 啟動多個讀者
for i := 0; i < 8; i++ {
wg.Add(1)
go reader()
}
// 單 writer依序更新
prices := []float64{1, 2, 3, 4, 5, 6, 7}
for _, px := range prices {
e.Update(d(int64(px)))
time.Sleep(1 * time.Millisecond)
}
// 收攤
close(stop)
wg.Wait()
// 最後至少應該是 ready
final := e.Load()
if !final.Ready {
t.Fatalf("final snapshot should be ready, got=%v", final.Ready)
2025-08-15 01:36:36 +00:00
}
}