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)) } // 產生參考 EMA(SMA-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,3(seed 完成 => 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) } }