194 lines
4.4 KiB
Go
194 lines
4.4 KiB
Go
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)
|
||
}()
|
||
}
|