blockchain/internal/lib/strategy/macd_test.go

224 lines
4.9 KiB
Go
Raw Normal View History

2025-08-17 14:48:13 +00:00
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")
}
}