blockchain/internal/lib/strategy/macd_test.go

194 lines
4.4 KiB
Go
Raw Normal View History

2025-08-17 14:48:13 +00:00
package strategy
import (
"github.com/shopspring/decimal"
2025-08-17 14:54:16 +00:00
"testing"
2025-08-17 14:48:13 +00:00
)
// --------------------------
// 參考實作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) {
2025-08-17 14:54:16 +00:00
// 讓交叉更容易出現:更短的週期 + 更長、先上後下的序列
fast, slow, signal := uint(2), uint(4), uint(2)
2025-08-17 14:48:13 +00:00
m := NewMACD(fast, slow, signal)
2025-08-17 14:54:16 +00:00
// 這段序列會:上漲 -> 回落 -> 再回升,通常至少會觸發一次交叉
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
)
2025-08-17 14:48:13 +00:00
for i, px := range seq {
out := m.Update(d(int64(px)))
2025-08-17 14:54:16 +00:00
// 還沒 ready 就繼續推進
2025-08-17 14:48:13 +00:00
if !out.Ready {
2025-08-17 14:54:16 +00:00
continue
}
if !seenReady {
// 第一次 ready初始化 prevHist
2025-08-17 14:48:13 +00:00
prevHist = out.Histogram
2025-08-17 14:54:16 +00:00
seenReady = true
2025-08-17 14:48:13 +00:00
continue
}
2025-08-17 14:54:16 +00:00
// 檢查正負翻轉(對應 MACDLine 與 SignalLine 的交叉)
if (prevHist.IsPositive() && out.Histogram.IsNegative()) ||
(prevHist.IsNegative() && out.Histogram.IsPositive()) {
foundFlip = true
break
2025-08-17 14:48:13 +00:00
}
prevHist = out.Histogram
2025-08-17 14:54:16 +00:00
// 安全閥(理論上不會到)
if i > 2000 {
2025-08-17 14:48:13 +00:00
break
}
}
2025-08-17 14:54:16 +00:00
2025-08-17 14:48:13 +00:00
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)
}()
}