blockchain/internal/lib/strategy/ema_test.go

171 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package strategy
import (
"sync"
"testing"
"time"
"github.com/shopspring/decimal"
)
func almostEqual(a, b decimal.Decimal, tol float64) bool {
if a.Equal(b) {
return true
}
diff := a.Sub(b).Abs()
return diff.LessThanOrEqual(decimal.NewFromFloat(tol))
}
// 產生參考 EMASMA-seed前 period 筆做 SMA 當種子,之後用 α 遞迴
func emaReferenceSMASeed(seq []float64, period uint) []decimal.Decimal {
if period == 0 {
panic("period must be > 0")
}
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
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
}
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)
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])
}
} else {
// 未 ready 時,檢查準備進度是否合理
if uint(i+1) == period && !out.Ready {
t.Fatalf("i=%d: should be ready at the period boundary", i)
}
}
}
}
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)
}
}