add sma ema macd
This commit is contained in:
parent
777a5952b8
commit
df849d98b7
|
@ -1,66 +1,107 @@
|
|||
package strategy
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
/*
|
||||
EMA,全名為指數移動平均線(Exponential Moving Average),用於平滑價格波動,幫助識別市場趨勢。
|
||||
它與簡單移動平均線(SMA)不同,EMA 更注重近期價格,因此對價格變動的反應更迅速,能更快地反映市場趨勢。
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
EMA 的主要特點和作用:
|
||||
更快速的反應:
|
||||
EMA 比 SMA 更快地反映價格變動,因為它給予近期數據更高的權重。
|
||||
識別趨勢:
|
||||
通過平滑價格波動,EMA 有助於識別市場的整體趨勢,判斷是上升趨勢還是下降趨勢。
|
||||
輔助交易決策:
|
||||
EMA 的使用可以幫助交易者判斷買入和賣出的時機,例如,當股價高於EMA 時,可能被視為買入信號;反之,則可能被視為賣出信號。
|
||||
適合短線交易:
|
||||
由於EMA 對價格變動的敏感性,它更適合於短線交易者,能更快地捕捉市場的短期波動。
|
||||
EMA 的計算方法:
|
||||
EMA 的計算涉及一個平滑因子和一個初始值,然後每天更新。 具體公式可以參考專業的金融網站或交易平台提供的資料。
|
||||
總結:
|
||||
EMA 是一種有用的技術分析工具,尤其適合於快速變動的市場,它可以幫助交易者更好地理解市場趨勢,並制定相應的交易策略
|
||||
*/
|
||||
// α = 2 / (N+1) 平滑係數
|
||||
func alphaFromPeriod(n uint) decimal.Decimal {
|
||||
return decimal.NewFromInt(2).
|
||||
Div(decimal.NewFromInt(int64(n)).Add(decimal.NewFromInt(1)))
|
||||
}
|
||||
|
||||
// =========================
|
||||
// EMA(單寫;快照多讀)
|
||||
// Seed: SMA 作為初始化
|
||||
// =========================
|
||||
|
||||
type emaCore struct {
|
||||
period uint
|
||||
alpha decimal.Decimal
|
||||
|
||||
// 狀態(單 writer 更新)
|
||||
value decimal.Decimal
|
||||
ready bool
|
||||
count uint
|
||||
sum decimal.Decimal // warm-up 期間用於 SMA seed
|
||||
}
|
||||
|
||||
func newEMACore(period uint) *emaCore {
|
||||
if period == 0 {
|
||||
panic("EMA period must be > 0")
|
||||
}
|
||||
return &emaCore{
|
||||
period: period,
|
||||
alpha: alphaFromPeriod(period),
|
||||
}
|
||||
}
|
||||
|
||||
// Push 僅供單 writer 呼叫
|
||||
// 回傳 (當前 EMA, ready)
|
||||
func (e *emaCore) Push(x decimal.Decimal) (decimal.Decimal, bool) {
|
||||
e.count++
|
||||
|
||||
// Warm-up:用 SMA 作為 seed
|
||||
if !e.ready {
|
||||
e.sum = e.sum.Add(x)
|
||||
if e.count == e.period {
|
||||
e.value = e.sum.Div(decimal.NewFromInt(int64(e.period)))
|
||||
e.ready = true
|
||||
}
|
||||
return e.value, e.ready
|
||||
}
|
||||
|
||||
oneMinus := decimal.NewFromInt(1).Sub(e.alpha)
|
||||
e.value = e.value.Mul(oneMinus).Add(x.Mul(e.alpha))
|
||||
return e.value, true
|
||||
}
|
||||
|
||||
// ===== 快照發布(多讀零鎖) =====
|
||||
|
||||
type EMASnapshot struct {
|
||||
Period uint
|
||||
Alpha string // 十進位字串,方便落盤或序列化
|
||||
Value decimal.Decimal // 目前 EMA
|
||||
Ready bool
|
||||
LastInput decimal.Decimal // 最近一次輸入值
|
||||
Count uint // 已處理資料點
|
||||
}
|
||||
|
||||
type EMA struct {
|
||||
n uint
|
||||
alp decimal.Decimal // 平滑係數 α = 2 / (n + 1)
|
||||
val decimal.Decimal // 當前EMA值
|
||||
ok bool // 內部旗標,用於判斷是否為第一筆資料
|
||||
core *emaCore
|
||||
last decimal.Decimal
|
||||
snap atomic.Value // holds EMASnapshot
|
||||
}
|
||||
|
||||
// NewEMA 建立EMA計算器
|
||||
func NewEMA(n uint) *EMA {
|
||||
return &EMA{
|
||||
n: n,
|
||||
alp: decimal.NewFromInt(2).Div(decimal.NewFromInt(int64(n + 1))),
|
||||
ok: false,
|
||||
}
|
||||
func NewEMA(period uint) *EMA {
|
||||
c := newEMACore(period)
|
||||
e := &EMA{core: c}
|
||||
e.snap.Store(EMASnapshot{
|
||||
Period: period,
|
||||
Alpha: c.alpha.String(),
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
// Push 輸入收盤價,返回當前EMA值
|
||||
func (e *EMA) Push(close decimal.Decimal) (decimal.Decimal, bool) {
|
||||
// 如果 n 無效,永遠回傳無效狀態
|
||||
if e.n == 0 {
|
||||
return decimal.Zero, false
|
||||
// Update 單 writer 呼叫
|
||||
func (e *EMA) Update(x decimal.Decimal) EMASnapshot {
|
||||
val, ready := e.core.Push(x)
|
||||
e.last = x
|
||||
snap := EMASnapshot{
|
||||
Period: e.core.period,
|
||||
Alpha: e.core.alpha.String(),
|
||||
Value: val,
|
||||
Ready: ready,
|
||||
LastInput: x,
|
||||
Count: e.core.count,
|
||||
}
|
||||
|
||||
if !e.ok {
|
||||
// 第一筆資料直接當作EMA初始值,並將狀態設為 ok
|
||||
e.val = close
|
||||
e.ok = true
|
||||
} else {
|
||||
// 後續資料使用 EMA 計算公式
|
||||
// EMA = α * close + (1 - α) * prev_EMA
|
||||
e.val = e.alp.Mul(close).Add(decimal.NewFromInt(1).Sub(e.alp).Mul(e.val))
|
||||
}
|
||||
// EMA 從第一筆資料開始就是有效的
|
||||
return e.val, true
|
||||
e.snap.Store(snap)
|
||||
return snap
|
||||
}
|
||||
|
||||
// GetEMA 取得目前 EMA 值
|
||||
func (e *EMA) GetEMA() (decimal.Decimal, bool) {
|
||||
if !e.ok {
|
||||
return decimal.Zero, false // 尚未初始化
|
||||
}
|
||||
return e.val, true
|
||||
// Load 多 reader 零鎖讀
|
||||
func (e *EMA) Load() EMASnapshot {
|
||||
return e.snap.Load().(EMASnapshot)
|
||||
}
|
||||
|
|
|
@ -1,102 +1,170 @@
|
|||
package strategy
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// --- EMA 的表格式驅動測試 (新增) ---
|
||||
func almostEqual(a, b decimal.Decimal, tol float64) bool {
|
||||
if a.Equal(b) {
|
||||
return true
|
||||
}
|
||||
diff := a.Sub(b).Abs()
|
||||
return diff.LessThanOrEqual(decimal.NewFromFloat(tol))
|
||||
}
|
||||
|
||||
func TestEMA(t *testing.T) {
|
||||
d10 := decimal.NewFromInt(10)
|
||||
d11 := decimal.NewFromInt(11)
|
||||
d12 := decimal.NewFromInt(12)
|
||||
d13 := decimal.NewFromInt(13)
|
||||
d20 := decimal.NewFromInt(20)
|
||||
// 產生參考 EMA(SMA-seed):前 period 筆做 SMA 當種子,之後用 α 遞迴
|
||||
func emaReferenceSMASeed(seq []float64, period uint) []decimal.Decimal {
|
||||
if period == 0 {
|
||||
panic("period must be > 0")
|
||||
}
|
||||
alpha := decimal.NewFromInt(2).
|
||||
Div(decimal.NewFromInt(int64(period)).Add(decimal.NewFromInt(1)))
|
||||
|
||||
type pushCheck struct {
|
||||
wantEMA decimal.Decimal
|
||||
wantOK bool
|
||||
out := make([]decimal.Decimal, len(seq))
|
||||
var sum decimal.Decimal
|
||||
var ready bool
|
||||
var emaVal decimal.Decimal
|
||||
|
||||
for i, x := range seq {
|
||||
px := d(int64(x))
|
||||
if !ready {
|
||||
sum = sum.Add(px)
|
||||
if uint(i+1) == period {
|
||||
emaVal = sum.Div(decimal.NewFromInt(int64(period))) // SMA seed
|
||||
ready = true
|
||||
}
|
||||
out[i] = emaVal
|
||||
continue
|
||||
}
|
||||
emaVal = emaVal.Mul(decimal.NewFromInt(1).Sub(alpha)).Add(px.Mul(alpha))
|
||||
out[i] = emaVal
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestEMA_WarmupAndStepByStep(t *testing.T) {
|
||||
type step struct {
|
||||
in float64
|
||||
want float64
|
||||
wantReady bool
|
||||
}
|
||||
alpha := 2.0 / (3.0 + 1.0) // period=3 => α=0.5
|
||||
_ = alpha
|
||||
|
||||
// 序列:1,2,3(seed 完成 => 2.0),下一筆 4 => 0.5*4 + 0.5*2 = 3.0
|
||||
steps := []step{
|
||||
{in: 1, want: 0, wantReady: false}, // value 在未 ready 前不重要,但實作會回 seed 累積值;這步不檢查值
|
||||
{in: 2, want: 0, wantReady: false},
|
||||
{in: 3, want: 2.0, wantReady: true},
|
||||
{in: 4, want: 3.0, wantReady: true},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
n uint
|
||||
inputs []decimal.Decimal
|
||||
pushChecks []pushCheck
|
||||
wantFinalEMA decimal.Decimal
|
||||
wantFinalOK bool
|
||||
}{
|
||||
{
|
||||
name: "EMA-3 標準計算",
|
||||
n: 3, // α = 2 / (3 + 1) = 0.5
|
||||
inputs: []decimal.Decimal{d10, d11, d12},
|
||||
pushChecks: []pushCheck{
|
||||
{d10, true}, // 第一次, EMA = 10
|
||||
{decimal.NewFromFloat(10.5), true}, // 第二次, 0.5*11 + (1-0.5)*10 = 5.5 + 5 = 10.5
|
||||
{decimal.NewFromFloat(11.25), true}, // 第三次, 0.5*12 + (1-0.5)*10.5 = 6 + 5.25 = 11.25
|
||||
},
|
||||
wantFinalEMA: decimal.NewFromFloat(11.25),
|
||||
wantFinalOK: true,
|
||||
},
|
||||
{
|
||||
name: "EMA-1 邊界情況",
|
||||
n: 1, // α = 2 / (1 + 1) = 1
|
||||
inputs: []decimal.Decimal{d10, d13, d11},
|
||||
pushChecks: []pushCheck{
|
||||
{d10, true}, // 第一次, EMA = 10
|
||||
{d13, true}, // 第二次, 1*13 + 0*10 = 13
|
||||
{d11, true}, // 第三次, 1*11 + 0*13 = 11
|
||||
},
|
||||
wantFinalEMA: d11,
|
||||
wantFinalOK: true,
|
||||
},
|
||||
{
|
||||
name: "EMA-0 無效情況",
|
||||
n: 0,
|
||||
inputs: []decimal.Decimal{d10, d20},
|
||||
pushChecks: []pushCheck{
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
},
|
||||
wantFinalEMA: decimal.Zero,
|
||||
wantFinalOK: false,
|
||||
},
|
||||
{
|
||||
name: "在空實例上呼叫 GetEMA",
|
||||
n: 5,
|
||||
inputs: []decimal.Decimal{},
|
||||
pushChecks: []pushCheck{},
|
||||
wantFinalEMA: decimal.Zero,
|
||||
wantFinalOK: false,
|
||||
},
|
||||
}
|
||||
e := NewEMA(3)
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ema := NewEMA(tc.n)
|
||||
|
||||
for i, input := range tc.inputs {
|
||||
gotEMA, gotOK := ema.Push(input)
|
||||
if i < len(tc.pushChecks) {
|
||||
check := tc.pushChecks[i]
|
||||
if gotOK != check.wantOK {
|
||||
t.Errorf("Push #%d 的 OK 狀態錯誤: got %v, want %v", i+1, gotOK, check.wantOK)
|
||||
}
|
||||
// 使用 String() 進行比較,避免浮點數精度問題
|
||||
if gotEMA.String() != check.wantEMA.String() {
|
||||
t.Errorf("Push #%d 的 EMA 值錯誤: got %s, want %s", i+1, gotEMA.String(), check.wantEMA.String())
|
||||
}
|
||||
}
|
||||
for i, st := range steps {
|
||||
got := e.Update(d(int64(st.in)))
|
||||
// 檢查 ready
|
||||
if got.Ready != st.wantReady {
|
||||
t.Fatalf("step %d: ready got=%v want=%v", i, got.Ready, st.wantReady)
|
||||
}
|
||||
// 檢查值(只有 ready 後才精準檢查)
|
||||
if got.Ready {
|
||||
if !almostEqual(got.Value, d(int64(st.want)), 1e-12) {
|
||||
t.Fatalf("step %d: value got=%s want=%v", i, got.Value, st.want)
|
||||
}
|
||||
|
||||
finalEMA, finalOK := ema.GetEMA()
|
||||
if finalOK != tc.wantFinalOK {
|
||||
t.Errorf("最終 GetEMA 的 OK 狀態錯誤: got %v, want %v", finalOK, tc.wantFinalOK)
|
||||
}
|
||||
if finalEMA.String() != tc.wantFinalEMA.String() {
|
||||
t.Errorf("最終 GetEMA 的 EMA 值錯誤: got %s, want %s", finalEMA.String(), tc.wantFinalEMA.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
// Load() 與 Update() 一致
|
||||
ld := e.Load()
|
||||
if ld.Ready != got.Ready || !ld.Value.Equal(got.Value) {
|
||||
t.Fatalf("step %d: Load() mismatch", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEMA_MatchesReference_SMASeed(t *testing.T) {
|
||||
period := uint(10)
|
||||
seq := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14, 15, 16, 15, 14, 13, 12, 11, 10, 11, 12, 13}
|
||||
ref := emaReferenceSMASeed(seq, period)
|
||||
|
||||
e := NewEMA(period)
|
||||
for i, x := range seq {
|
||||
out := e.Update(d(int64(x)))
|
||||
// 只有在 ready 後才對齊 ref
|
||||
if out.Ready {
|
||||
if !almostEqual(out.Value, ref[i], 1e-9) {
|
||||
t.Fatalf("i=%d: EMA value got=%s ref=%s", i, out.Value, ref[i])
|
||||
}
|
||||
} else {
|
||||
// 未 ready 時,檢查準備進度是否合理
|
||||
if uint(i+1) == period && !out.Ready {
|
||||
t.Fatalf("i=%d: should be ready at the period boundary", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEMA_PeriodOne_DegenerateCase(t *testing.T) {
|
||||
// period=1 => α=1,第一筆即 ready,之後永遠等於最新價
|
||||
e := NewEMA(1)
|
||||
seq := []float64{10, 11, 12, 9, 8, 15}
|
||||
for i, x := range seq {
|
||||
out := e.Update(d(int64(x)))
|
||||
if !out.Ready {
|
||||
t.Fatalf("i=%d: period=1 should be ready immediately", i)
|
||||
}
|
||||
if !out.Value.Equal(d(int64(x))) {
|
||||
t.Fatalf("i=%d: value got=%s want=%v", i, out.Value, x)
|
||||
}
|
||||
// Load() 同步
|
||||
if !e.Load().Value.Equal(out.Value) {
|
||||
t.Fatalf("i=%d: Load() mismatch", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEMA_MultiReadersSingleWriter(t *testing.T) {
|
||||
e := NewEMA(5)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
stop := make(chan struct{})
|
||||
|
||||
// 多 reader:不斷 Load(),不應當出現資料競態或 panic
|
||||
reader := func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
_ = e.Load()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 啟動多個讀者
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go reader()
|
||||
}
|
||||
|
||||
// 單 writer:依序更新
|
||||
prices := []float64{1, 2, 3, 4, 5, 6, 7}
|
||||
for _, px := range prices {
|
||||
e.Update(d(int64(px)))
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 收攤
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
|
||||
// 最後至少應該是 ready
|
||||
final := e.Load()
|
||||
if !final.Ready {
|
||||
t.Fatalf("final snapshot should be ready, got=%v", final.Ready)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
package strategy
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// =========================
|
||||
// MACD(單寫;快照多讀)
|
||||
// fastEMA / slowEMA 吃「收盤價」;signalEMA 吃「MACDLine」
|
||||
// 與 EMA/SMA 一致風格:Update()(單寫)、Load()(多讀)
|
||||
// =========================
|
||||
|
||||
// MACDSnapshot 對外發佈的不可變快照
|
||||
type MACDSnapshot struct {
|
||||
Params [3]uint // (fast, slow, signal)
|
||||
LastPrice decimal.Decimal // 最近一次喂入的收盤價
|
||||
MACDLine decimal.Decimal // DIF = fastEMA - slowEMA
|
||||
SignalLine decimal.Decimal // DEA = EMA(MACDLine, signalPeriod)
|
||||
Histogram decimal.Decimal // Hist = MACDLine - SignalLine
|
||||
Ready bool // 三條線皆 ready 才會 true
|
||||
}
|
||||
|
||||
// MACD 快照發佈器(單寫多讀)
|
||||
type MACD struct {
|
||||
// 內部用你已完成的 EMA(其本身就含 SMA-seed 與 ready 狀態)
|
||||
fastEMA *EMA
|
||||
slowEMA *EMA
|
||||
signalEMA *EMA
|
||||
|
||||
fastPeriod, slowPeriod, signalPeriod uint
|
||||
|
||||
last decimal.Decimal // 只作紀錄(debug/觀察用)
|
||||
snap atomic.Value // holds MACDSnapshot
|
||||
}
|
||||
|
||||
// NewMACD 建立 MACD(預設 12,26,9;也可自訂)
|
||||
func NewMACD(fast, slow, signal uint) *MACD {
|
||||
if !(fast > 0 && slow > 0 && signal > 0) {
|
||||
panic("MACD periods must be > 0")
|
||||
}
|
||||
if fast >= slow {
|
||||
panic("MACD requires fast < slow (e.g., 12 < 26)")
|
||||
}
|
||||
|
||||
m := &MACD{
|
||||
fastEMA: NewEMA(fast),
|
||||
slowEMA: NewEMA(slow),
|
||||
signalEMA: NewEMA(signal),
|
||||
fastPeriod: fast,
|
||||
slowPeriod: slow,
|
||||
signalPeriod: signal,
|
||||
}
|
||||
// 初始化一個空快照
|
||||
m.snap.Store(MACDSnapshot{Params: [3]uint{fast, slow, signal}})
|
||||
return m
|
||||
}
|
||||
|
||||
// Update 僅供單一 writer 呼叫;回傳並發佈最新快照
|
||||
func (m *MACD) Update(close decimal.Decimal) MACDSnapshot {
|
||||
// 先用 EMA(你已改好的版本)各自更新快、慢線
|
||||
fast := m.fastEMA.Update(close)
|
||||
slow := m.slowEMA.Update(close)
|
||||
|
||||
macdLine := fast.Value.Sub(slow.Value)
|
||||
|
||||
// 信號線吃的是 MACDLine(非收盤價)
|
||||
sig := m.signalEMA.Update(macdLine)
|
||||
|
||||
ready := fast.Ready && slow.Ready && sig.Ready
|
||||
|
||||
snap := MACDSnapshot{
|
||||
Params: [3]uint{m.fastPeriod, m.slowPeriod, m.signalPeriod},
|
||||
LastPrice: close,
|
||||
MACDLine: macdLine,
|
||||
SignalLine: sig.Value,
|
||||
Histogram: macdLine.Sub(sig.Value),
|
||||
Ready: ready,
|
||||
}
|
||||
m.last = close
|
||||
m.snap.Store(snap)
|
||||
return snap
|
||||
}
|
||||
|
||||
// Load 多 goroutine 零鎖讀取最新快照
|
||||
func (m *MACD) Load() MACDSnapshot {
|
||||
return m.snap.Load().(MACDSnapshot)
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
# 技術指標整理:SMA、EMA、MACD
|
||||
|
||||
## 1. SMA (Simple Moving Average)
|
||||
**原理**
|
||||
- 計算某段時間內價格的「算術平均值」。
|
||||
- 例如:20 日 SMA = 最近 20 天收盤價的平均值。
|
||||
### 公式
|
||||
* 簡單地將一段時間內的收盤價加總,再除以該期間的交易日數。 例如,5 日SMA 就是將過去 5 個交易日的收盤價加總後除以 5。
|
||||
|
||||
|
||||
**使用時機**
|
||||
- 想要觀察「長期趨勢」時(例如 50 日、200 日)。
|
||||
- 作為 **支撐 / 壓力線** 參考。
|
||||
|
||||
**優點**
|
||||
- 簡單易懂,市場上最常見的基準。
|
||||
- 適合長線投資者,看清楚大趨勢。
|
||||
|
||||
**缺點**
|
||||
- 對價格變動反應慢(容易「滯後」)。
|
||||
- 在震盪盤中容易給出假訊號。
|
||||
|
||||
### SMA – 大趨勢濾網
|
||||
- 使用時機:想要知道市場長期偏多還是偏空。
|
||||
- 案例:台股站上 200 日 SMA → 牛市傾向。
|
||||
|
||||
---
|
||||
|
||||
## 2. EMA (Exponential Moving Average)
|
||||
**原理**
|
||||
- 對「近期價格」給予更高權重的移動平均。
|
||||
- 例如:20 日 EMA 會比 20 日 SMA 更快跟上價格。
|
||||
|
||||
## EMA(指數移動平均)設計的目標是:
|
||||
* 越新的資料 → 權重越大。
|
||||
* 越舊的資料 → 權重遞減,但不會突然歸零。
|
||||
### 公式
|
||||
* EMA = α * close + (1 - α) * prev_EMA
|
||||
### 其中:
|
||||
* close = 當期價格
|
||||
* prev_EMA = 上期 EMA
|
||||
* α = 平滑係數 (0~1 之間)
|
||||
|
||||
### 問題在:怎麼選 α 才合理?
|
||||
這裡的依據是要讓 EMA 的「有效週期」接近 N。
|
||||
* 如果用 SMA(簡單移動平均),每一筆資料在 N 期內的權重相等 = 1/N。
|
||||
* 但 EMA 要設計成:最近資料權重大,舊資料權重指數衰減。
|
||||
|
||||
透過數學推導(加權和 = 1,且平均壽命接近 N),得到:
|
||||
α = 2/(n+1)
|
||||
|
||||
### 👉 這樣設計的結果是:
|
||||
1. EMA 的「記憶長度」大約等於 N。
|
||||
2. 和 N 日 SMA 的「平滑程度」接近,但又能更快反應新價格。
|
||||
|
||||
* **使用時機**
|
||||
- 想要「更快捕捉趨勢」的交易者。
|
||||
- 常用於短中期判斷,例如 12 日、26 日 EMA。
|
||||
|
||||
**優點**
|
||||
- 反應快,能更快抓到趨勢轉折。
|
||||
- 適合短線與波段交易者。
|
||||
|
||||
**缺點**
|
||||
- 容易被假突破影響,訊號較「吵」。
|
||||
- 在盤整時誤導訊號比 SMA 更多。
|
||||
|
||||
### EMA – 快速抓轉折
|
||||
- 使用時機:提早嗅到行情的變化,適合短線/波段。
|
||||
- 案例:BTC 出現 12/26 EMA 黃金交叉 → 多頭信號。
|
||||
---
|
||||
|
||||
## 3. MACD (Moving Average Convergence Divergence)
|
||||
**原理**
|
||||
- 由兩條 EMA (快線、慢線) 的差值,再加上訊號線組成。
|
||||
- MACD 線 = 12 日 EMA − 26 日 EMA
|
||||
- 訊號線 = MACD 線的 9 日 EMA
|
||||
- 用來判斷「動能」與「趨勢強弱」。
|
||||
|
||||
**使用時機**
|
||||
- 當價格趨勢明顯時,MACD 很有用。
|
||||
- 適合判斷「多空動能轉換」、「背離現象」。
|
||||
- 常搭配交叉訊號使用:
|
||||
- MACD 上穿訊號線 → 看多
|
||||
- MACD 下穿訊號線 → 看空
|
||||
|
||||
**優點**
|
||||
- 不只是趨勢,還能判斷「動能強弱」。
|
||||
- 有交叉、背離、柱狀圖多種訊號。
|
||||
|
||||
**缺點**
|
||||
- 還是屬於「落後指標」,轉折不會在第一時間。
|
||||
- 在盤整行情中也會有很多假訊號。
|
||||
|
||||
### MACD – 動能 & 背離
|
||||
- 使用時機:想知道趨勢是否有力氣繼續。
|
||||
- 案例:價格創新高,但 MACD 沒創新高 → 頂背離,可能反轉。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 4. 使用上的限制
|
||||
1. **都是「落後指標」**
|
||||
- 不會在第一時間告訴你轉折,只是確認趨勢。
|
||||
|
||||
2. **震盪行情容易誤導**
|
||||
- 當價格在小區間來回,SMA/EMA 會不斷交叉,MACD 也會亂跳。
|
||||
|
||||
3. **參數選擇影響很大**
|
||||
- 太短 → 訊號過於頻繁。太長 → 錯過行情。
|
||||
|
||||
## 5. 三者組合策略
|
||||
|
||||
1. **SMA (200 日)**:判斷長期方向。
|
||||
- 價格在 200 日 SMA 上方 → 偏多操作。
|
||||
|
||||
2. **EMA (12 日 & 26 日)**:判斷短中期趨勢。
|
||||
- 12 日 EMA 上穿 26 日 EMA → 留意進場。
|
||||
|
||||
3. **MACD**:確認動能。
|
||||
- MACD 黃金交叉,柱狀圖翻正 → 動能支持。
|
||||
|
||||
**實戰例子**:
|
||||
- 2020 年 4 月,比特幣站上 200 日 SMA + EMA 黃金交叉 + MACD 翻正 → 開啟牛市行情。
|
||||
|
||||
---
|
||||
|
||||
## 6. 總結比喻
|
||||
- **SMA** = 老師傅,看大方向。
|
||||
- **EMA** = 年輕駕駛,反應快但容易緊張。
|
||||
- **MACD** = 汽車轉速表,看力道強不強。
|
||||
|
||||
組合起來就像開車上高速公路:
|
||||
- 先看導航(SMA)。
|
||||
- 再看方向盤反應(EMA)。
|
||||
- 最後看轉速表(MACD)。
|
|
@ -1,46 +1,103 @@
|
|||
package strategy
|
||||
|
||||
import "github.com/shopspring/decimal"
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
/* SMA (Simple Moving Average) 簡單移動平均線。 它透過計算一段時間內股價的平均值,來判斷趨勢和提供交易信號。
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
SMA 的計算方式:
|
||||
SMA 簡單地將一段時間內的收盤價加總,再除以該期間的交易日數。 例如,5 日SMA 就是將過去 5 個交易日的收盤價加總後除以 5。
|
||||
SMA 的應用:
|
||||
1. 趨勢判斷:
|
||||
SMA 可以用來判斷價格的趨勢。 當股價在 SMA 上方,且 SMA 向上移動時,表示股價處於上升趨勢;
|
||||
反之,當股價在 SMA 下方,且SMA 向下移動時,表示股價處於下降趨勢。
|
||||
2. 支撐與阻力:
|
||||
SMA 也可以被視為支撐位和阻力位。 當股價下跌到SMA 附近時,SMA 可能會提供支撐;而當股價上漲到SMA 附近時,SMA 可能會提供阻力。
|
||||
3. 交易信號:
|
||||
移動平均線的交叉也可以產生交易信號。 例如,當短期SMA 線向上穿越長期SMA 線時,被稱為黃金交叉,可能是一個買入信號;
|
||||
反之,當短期SMA 線向下穿越長期SMA 線時,被稱為死亡交叉,可能是一個賣出信號。
|
||||
SMA 的優缺點:
|
||||
優點: SMA 計算簡單、易於理解,適合新手使用。
|
||||
缺點: SMA 對於價格變動的反應較為遲鈍,可能會落後於市場,特別是短期波動時,可能不如其他移動平均線指標準確。
|
||||
*/
|
||||
// =========================
|
||||
// SMA(固定窗寬;單寫多讀 + atomic 快照)
|
||||
// =========================
|
||||
|
||||
// smaCore:單 writer 的核心計算(ring buffer + 滑動平均)
|
||||
type smaCore struct {
|
||||
window uint
|
||||
|
||||
// 狀態(單 writer)
|
||||
buf []decimal.Decimal // ring buffer
|
||||
head int // 下一個覆蓋位置
|
||||
size uint // 目前填充數量(<= window)
|
||||
sum decimal.Decimal
|
||||
ready bool
|
||||
}
|
||||
|
||||
func newSMACore(window uint) *smaCore {
|
||||
if window == 0 {
|
||||
panic("SMA window must be > 0")
|
||||
}
|
||||
|
||||
return &smaCore{
|
||||
window: window,
|
||||
buf: make([]decimal.Decimal, window),
|
||||
}
|
||||
}
|
||||
|
||||
// Push 只能被**單一** goroutine(writer)呼叫。
|
||||
func (s *smaCore) Push(x decimal.Decimal) (decimal.Decimal, bool) {
|
||||
// 填充期
|
||||
if s.size < s.window {
|
||||
s.buf[s.head] = x
|
||||
s.sum = s.sum.Add(x)
|
||||
s.head = (s.head + 1) % int(s.window)
|
||||
s.size++
|
||||
if s.size == s.window {
|
||||
s.ready = true
|
||||
}
|
||||
|
||||
return s.sum.Div(decimal.NewFromInt(int64(s.size))), s.ready
|
||||
}
|
||||
|
||||
// 滿窗:移除最舊,加入最新
|
||||
old := s.buf[s.head]
|
||||
s.buf[s.head] = x
|
||||
s.head = (s.head + 1) % int(s.window)
|
||||
|
||||
s.sum = s.sum.Sub(old).Add(x)
|
||||
avg := s.sum.Div(decimal.NewFromInt(int64(s.window)))
|
||||
|
||||
return avg, true
|
||||
}
|
||||
|
||||
// SMASnapshot 對外發佈的不可變快照(多 reader 零鎖 Load)
|
||||
type SMASnapshot struct {
|
||||
Window uint
|
||||
Value decimal.Decimal
|
||||
Ready bool
|
||||
LastInput decimal.Decimal
|
||||
Size uint // 當前填充數量(<=Window)
|
||||
}
|
||||
|
||||
// SMA 對外使用的快照發佈器(單寫多讀)
|
||||
type SMA struct {
|
||||
q *ringQD
|
||||
core *smaCore
|
||||
|
||||
last decimal.Decimal
|
||||
snap atomic.Value // holds SMASnapshot
|
||||
}
|
||||
|
||||
// NewSMA 建立SMA計算器
|
||||
func NewSMA(n uint) *SMA { return &SMA{q: newRingQD(n)} }
|
||||
func NewSMA(window uint) *SMA {
|
||||
s := &SMA{core: newSMACore(window)}
|
||||
s.snap.Store(SMASnapshot{Window: window})
|
||||
return s
|
||||
}
|
||||
|
||||
// Push 輸入收盤價,返回當前SMA值
|
||||
func (s *SMA) Push(close decimal.Decimal) (decimal.Decimal, bool) {
|
||||
s.q.push(close)
|
||||
if !s.q.ready() {
|
||||
return decimal.Zero, false // 尚未湊滿資料
|
||||
// Update 僅允許**單一 writer**呼叫
|
||||
func (s *SMA) Update(x decimal.Decimal) SMASnapshot {
|
||||
val, ready := s.core.Push(x)
|
||||
s.last = x
|
||||
ss := SMASnapshot{
|
||||
Window: s.core.window,
|
||||
Value: val,
|
||||
Ready: ready,
|
||||
LastInput: x,
|
||||
Size: s.core.size,
|
||||
}
|
||||
return s.q.mean(), true
|
||||
s.snap.Store(ss)
|
||||
return ss
|
||||
}
|
||||
|
||||
// GetSMA 取得目前 SMA 值
|
||||
func (s *SMA) GetSMA() (decimal.Decimal, bool) {
|
||||
if !s.q.ready() {
|
||||
return decimal.Zero, false // 尚未湊滿資料
|
||||
}
|
||||
|
||||
return s.q.mean(), true
|
||||
// Load 多 reader 零鎖讀最新快照
|
||||
func (s *SMA) Load() SMASnapshot {
|
||||
return s.snap.Load().(SMASnapshot)
|
||||
}
|
||||
|
|
|
@ -1,132 +1,120 @@
|
|||
package strategy
|
||||
|
||||
import (
|
||||
"github.com/shopspring/decimal"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// --- SMA 的表格式驅動測試 ---
|
||||
func d(i int64) decimal.Decimal { return decimal.NewFromInt(i) }
|
||||
|
||||
func TestSMA(t *testing.T) {
|
||||
d10 := decimal.NewFromInt(10)
|
||||
d20 := decimal.NewFromInt(20)
|
||||
d30 := decimal.NewFromInt(30)
|
||||
d40 := decimal.NewFromInt(40)
|
||||
d50 := decimal.NewFromInt(50)
|
||||
|
||||
// 定義 Push 過程中的檢查點結構
|
||||
type pushCheck struct {
|
||||
wantSMA decimal.Decimal
|
||||
wantOK bool
|
||||
func TestSMA_WarmupAndSliding(t *testing.T) {
|
||||
type step struct {
|
||||
in int64
|
||||
wantVal string // 用字串比對可避免浮點誤差(decimal 本就精準)
|
||||
ready bool
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
n uint
|
||||
inputs []decimal.Decimal
|
||||
pushChecks []pushCheck // 驗證每一次 Push 的回傳值
|
||||
wantFinalSMA decimal.Decimal // 驗證最後 GetSMA 的回傳值
|
||||
wantFinalOK bool
|
||||
tests := []struct {
|
||||
name string
|
||||
window uint
|
||||
steps []step
|
||||
}{
|
||||
{
|
||||
name: "SMA-5 未滿載",
|
||||
n: 5,
|
||||
inputs: []decimal.Decimal{d10, d20, d30},
|
||||
pushChecks: []pushCheck{
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
name: "warmup_then_ready_and_slide",
|
||||
window: 3,
|
||||
steps: []step{
|
||||
{in: 1, wantVal: "1", ready: false}, // [1] avg=1
|
||||
{in: 2, wantVal: "1.5", ready: false}, // [1,2] avg=1.5
|
||||
{in: 3, wantVal: "2", ready: true}, // [1,2,3] avg=2
|
||||
{in: 4, wantVal: "3", ready: true}, // [2,3,4] avg=3
|
||||
{in: 5, wantVal: "4", ready: true}, // [3,4,5] avg=4
|
||||
{in: 6, wantVal: "5", ready: true}, // [4,5,6] avg=5
|
||||
{in: 7, wantVal: "6", ready: true}, // [5,6,7] avg=6
|
||||
},
|
||||
wantFinalSMA: decimal.Zero,
|
||||
wantFinalOK: false,
|
||||
},
|
||||
{
|
||||
name: "SMA-3 剛好滿載",
|
||||
n: 3,
|
||||
inputs: []decimal.Decimal{d10, d20, d30},
|
||||
pushChecks: []pushCheck{
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
{decimal.NewFromInt(20), true}, // (10+20+30)/3
|
||||
name: "window_1_behaves_as_latest_value",
|
||||
window: 1,
|
||||
steps: []step{
|
||||
{in: 10, wantVal: "10", ready: true},
|
||||
{in: 11, wantVal: "11", ready: true},
|
||||
{in: 12, wantVal: "12", ready: true},
|
||||
},
|
||||
wantFinalSMA: decimal.NewFromInt(20),
|
||||
wantFinalOK: true,
|
||||
},
|
||||
{
|
||||
name: "SMA-3 滾動計算",
|
||||
n: 3,
|
||||
inputs: []decimal.Decimal{d10, d20, d30, d40, d50},
|
||||
pushChecks: []pushCheck{
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
{decimal.NewFromInt(20), true}, // (10+20+30)/3
|
||||
{decimal.NewFromInt(30), true}, // (20+30+40)/3
|
||||
{decimal.NewFromInt(40), true}, // (30+40+50)/3
|
||||
},
|
||||
wantFinalSMA: decimal.NewFromInt(40),
|
||||
wantFinalOK: true,
|
||||
},
|
||||
{
|
||||
name: "SMA-1 邊界情況",
|
||||
n: 1,
|
||||
inputs: []decimal.Decimal{d10, d20, d30},
|
||||
pushChecks: []pushCheck{
|
||||
{d10, true},
|
||||
{d20, true},
|
||||
{d30, true},
|
||||
},
|
||||
wantFinalSMA: d30,
|
||||
wantFinalOK: true,
|
||||
},
|
||||
{
|
||||
name: "SMA-0 無效情況",
|
||||
n: 0,
|
||||
inputs: []decimal.Decimal{d10, d20, d30},
|
||||
pushChecks: []pushCheck{
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
{decimal.Zero, false},
|
||||
},
|
||||
wantFinalSMA: decimal.Zero,
|
||||
wantFinalOK: false,
|
||||
},
|
||||
{
|
||||
name: "在空實例上呼叫 GetSMA",
|
||||
n: 5,
|
||||
inputs: []decimal.Decimal{},
|
||||
pushChecks: []pushCheck{},
|
||||
wantFinalSMA: decimal.Zero,
|
||||
wantFinalOK: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
sma := NewSMA(tc.n)
|
||||
|
||||
// 驗證每一次 Push 的結果
|
||||
for i, input := range tc.inputs {
|
||||
gotSMA, gotOK := sma.Push(input)
|
||||
// 確保 pushChecks 陣列不會索引越界
|
||||
if i < len(tc.pushChecks) {
|
||||
check := tc.pushChecks[i]
|
||||
if gotOK != check.wantOK {
|
||||
t.Errorf("Push #%d 的 OK 狀態錯誤: got %v, want %v", i+1, gotOK, check.wantOK)
|
||||
}
|
||||
if !gotSMA.Equals(check.wantSMA) {
|
||||
t.Errorf("Push #%d 的 SMA 值錯誤: got %s, want %s", i+1, gotSMA.String(), check.wantSMA.String())
|
||||
}
|
||||
s := NewSMA(tc.window)
|
||||
for i, st := range tc.steps {
|
||||
got := s.Update(d(st.in))
|
||||
if got.Value.String() != st.wantVal {
|
||||
t.Fatalf("step %d: got value %s, want %s", i, got.Value, st.wantVal)
|
||||
}
|
||||
if got.Ready != st.ready {
|
||||
t.Fatalf("step %d: ready mismatch, got %v, want %v", i, got.Ready, st.ready)
|
||||
}
|
||||
// Load() 應該等於最新快照
|
||||
ld := s.Load()
|
||||
if ld.Value.String() != st.wantVal || ld.Ready != st.ready {
|
||||
t.Fatalf("step %d: Load() not latest snapshot", i)
|
||||
}
|
||||
}
|
||||
|
||||
// 在所有 Push 操作完成後,驗證最終 GetSMA 的結果
|
||||
finalSMA, finalOK := sma.GetSMA()
|
||||
if finalOK != tc.wantFinalOK {
|
||||
t.Errorf("最終 GetSMA 的 OK 狀態錯誤: got %v, want %v", finalOK, tc.wantFinalOK)
|
||||
}
|
||||
if !finalSMA.Equals(tc.wantFinalSMA) {
|
||||
t.Errorf("最終 GetSMA 的 SMA 值錯誤: got %s, want %s", finalSMA.String(), tc.wantFinalSMA.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSMA_NewZeroWindowShouldPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Fatalf("expected panic when window=0")
|
||||
}
|
||||
}()
|
||||
_ = NewSMA(0)
|
||||
}
|
||||
|
||||
func TestSMA_MultiReadersSingleWriter(t *testing.T) {
|
||||
s := NewSMA(3)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
stop := make(chan struct{})
|
||||
|
||||
// 多 reader 併發讀取(零鎖)
|
||||
reader := func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
default:
|
||||
_ = s.Load() // 不做判斷,重點是不得 panic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 啟動多個讀者
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go reader()
|
||||
}
|
||||
|
||||
// 單 writer 更新
|
||||
prices := []int64{1, 2, 3, 4, 5, 6, 7}
|
||||
for _, p := range prices {
|
||||
s.Update(d(p))
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 停 reader
|
||||
close(stop)
|
||||
wg.Wait()
|
||||
|
||||
// 最終應為 [5,6,7] 的平均=6,且 ready=true
|
||||
got := s.Load()
|
||||
if got.Value.String() != "6" || !got.Ready {
|
||||
t.Fatalf("final snapshot mismatch: got value=%s ready=%v", got.Value, got.Ready)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue