blockchain/internal/lib/strategy/macd_test.go

194 lines
4.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}()
}