package strategy import ( "sync" "testing" "time" "github.com/shopspring/decimal" ) // -------------------------- // 參考實作(SMA-seed 的 EMA / MACD) // -------------------------- type refEMA struct { period uint alpha decimal.Decimal count uint sum decimal.Decimal ready bool value decimal.Decimal } func newRefEMA(period uint) *refEMA { if period == 0 { panic("period must be > 0") } return &refEMA{ period: period, alpha: decimal.NewFromInt(2). Div(decimal.NewFromInt(int64(period)).Add(decimal.NewFromInt(1))), } } func (e *refEMA) Update(x decimal.Decimal) (val decimal.Decimal, ready bool) { e.count++ if !e.ready { e.sum = e.sum.Add(x) if e.count == e.period { e.value = e.sum.Div(decimal.NewFromInt(int64(e.period))) // SMA seed e.ready = true } return e.value, e.ready } e.value = e.value.Mul(decimal.NewFromInt(1).Sub(e.alpha)).Add(x.Mul(e.alpha)) return e.value, true } type refMACD struct { fast, slow, signal *refEMA } type refMACDOut struct { MACDLine, SignalLine, Hist decimal.Decimal Ready bool } func newRefMACD(fast, slow, signal uint) *refMACD { return &refMACD{ fast: newRefEMA(fast), slow: newRefEMA(slow), signal: newRefEMA(signal), } } func (m *refMACD) Update(close decimal.Decimal) refMACDOut { fv, fr := m.fast.Update(close) sv, sr := m.slow.Update(close) macd := fv.Sub(sv) sig, sready := m.signal.Update(macd) ready := fr && sr && sready return refMACDOut{ MACDLine: macd, SignalLine: sig, Hist: macd.Sub(sig), Ready: ready, } } // -------------------------- // Tests // -------------------------- func TestMACD_WarmupAndStepByStep(t *testing.T) { // 用較短的參數讓 warm-up 與變化更明顯 fast, slow, signal := uint(3), uint(6), uint(3) m := NewMACD(fast, slow, signal) ref := newRefMACD(fast, slow, signal) seq := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14} for i, f := range seq { out := m.Update(d(int64(f))) ro := ref.Update(d(int64(f))) // Ready 條件:三條 EMA 都完成各自 seed if out.Ready != ro.Ready { t.Fatalf("i=%d: ready mismatch got=%v want(ref)=%v", i, out.Ready, ro.Ready) } // Ready 後數值應與參考一致(decimal 精準,可直接 Equal) if out.Ready { if !out.MACDLine.Equal(ro.MACDLine) || !out.SignalLine.Equal(ro.SignalLine) || !out.Histogram.Equal(ro.Hist) { t.Fatalf("i=%d: values mismatch\ngot: dif=%s dea=%s hist=%s\nwant: dif=%s dea=%s hist=%s", i, out.MACDLine, out.SignalLine, out.Histogram, ro.MACDLine, ro.SignalLine, ro.Hist) } } // Load 與 Update 發佈的一致 ld := m.Load() if ld.Ready != out.Ready || !ld.MACDLine.Equal(out.MACDLine) || !ld.SignalLine.Equal(out.SignalLine) || !ld.Histogram.Equal(out.Histogram) { t.Fatalf("i=%d: Load() snapshot mismatch", i) } } } func TestMACD_HistogramSignFlip_CrossSignal(t *testing.T) { // 讓交叉更容易出現:短週期 fast, slow, signal := uint(3), uint(6), uint(3) m := NewMACD(fast, slow, signal) // 人工設計一段先上後下的序列,觀察 Histogram 正負翻轉 seq := []float64{10, 11, 12, 13, 14, 13, 12, 11, 10, 9} var prevReady bool var prevHist decimal.Decimal foundFlip := false for i, px := range seq { out := m.Update(d(int64(px))) if !out.Ready { prevReady = out.Ready prevHist = out.Histogram continue } if prevReady { // 正 -> 負 或 負 -> 正 視為切換近似(對應 MACDLine 與 SignalLine 交叉) if (prevHist.IsPositive() && out.Histogram.IsNegative()) || (prevHist.IsNegative() && out.Histogram.IsPositive()) { foundFlip = true break } } prevReady = out.Ready prevHist = out.Histogram if i > 1000 { // 安全閥 break } } if !foundFlip { t.Fatalf("expected at least one histogram sign flip (cross), but not found") } } func TestMACD_ParamsValidation(t *testing.T) { // fast >= slow 應 panic func() { defer func() { if r := recover(); r == nil { t.Fatalf("expected panic when fast >= slow") } }() _ = NewMACD(12, 12, 9) }() // 任一參數為 0 應 panic func() { defer func() { if r := recover(); r == nil { t.Fatalf("expected panic when any period is 0") } }() _ = NewMACD(0, 26, 9) }() } func TestMACD_MultiReadersSingleWriter(t *testing.T) { m := NewMACD(12, 26, 9) var wg sync.WaitGroup stop := make(chan struct{}) // 多 reader:不斷 Load(),不應出現競態或 panic reader := func() { defer wg.Done() for { select { case <-stop: return default: _ = m.Load() } } } // 啟動多個讀者 for i := 0; i < 8; i++ { wg.Add(1) go reader() } // 單 writer:依序更新 prices := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14, 15, 16, 15, 14} for _, px := range prices { m.Update(d(int64(px))) time.Sleep(1 * time.Millisecond) } // 收攤 close(stop) wg.Wait() // 最後應該已經 ready final := m.Load() if !final.Ready { t.Fatalf("final snapshot should be ready") } }