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