package strategy import ( "github.com/shopspring/decimal" "testing" ) // -------------------------- // 參考實作(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(2), uint(4), uint(2) m := NewMACD(fast, slow, signal) // 這段序列會:上漲 -> 回落 -> 再回升,通常至少會觸發一次交叉 seq := []float64{ 10, 10.5, 11, 11.5, 12, 12.5, // 上 12, 11.5, 11, 10.5, 10, 9.5, // 下 10, 10.5, 11, // 再上 } // 等 ready 之後再看柱狀圖正負翻轉 var ( seenReady bool prevHist decimal.Decimal foundFlip bool ) for i, px := range seq { out := m.Update(d(int64(px))) // 還沒 ready 就繼續推進 if !out.Ready { continue } if !seenReady { // 第一次 ready,初始化 prevHist prevHist = out.Histogram seenReady = true continue } // 檢查正負翻轉(對應 MACDLine 與 SignalLine 的交叉) if (prevHist.IsPositive() && out.Histogram.IsNegative()) || (prevHist.IsNegative() && out.Histogram.IsPositive()) { foundFlip = true break } prevHist = out.Histogram // 安全閥(理論上不會到) if i > 2000 { 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) }() }