blockchain/internal/lib/strategy/macd_test.go

224 lines
4.9 KiB
Go
Raw 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 (
"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")
}
}