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