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
|
|
|
|
// 產生參考 EMA(SMA-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,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)
|
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
|
|
|
|
}
|
|
|
|
|
}
|