From 66b734350dcfc62796c35a4f7e90447660f08024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Sun, 17 Aug 2025 22:54:16 +0800 Subject: [PATCH] add sma ema macd --- internal/lib/strategy/macd_test.go | 100 ++++++------------ internal/lib/strategy/ring_queue.go | 55 ---------- internal/lib/strategy/ring_queue_test.go | 129 ----------------------- 3 files changed, 35 insertions(+), 249 deletions(-) delete mode 100644 internal/lib/strategy/ring_queue.go delete mode 100644 internal/lib/strategy/ring_queue_test.go diff --git a/internal/lib/strategy/macd_test.go b/internal/lib/strategy/macd_test.go index 8a0c6e6..33ecd67 100644 --- a/internal/lib/strategy/macd_test.go +++ b/internal/lib/strategy/macd_test.go @@ -1,11 +1,8 @@ package strategy import ( - "sync" - "testing" - "time" - "github.com/shopspring/decimal" + "testing" ) // -------------------------- @@ -121,37 +118,53 @@ func TestMACD_WarmupAndStepByStep(t *testing.T) { } func TestMACD_HistogramSignFlip_CrossSignal(t *testing.T) { - // 讓交叉更容易出現:短週期 - fast, slow, signal := uint(3), uint(6), uint(3) + // 讓交叉更容易出現:更短的週期 + 更長、先上後下的序列 + fast, slow, signal := uint(2), uint(4), uint(2) 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 + // 這段序列會:上漲 -> 回落 -> 再回升,通常至少會觸發一次交叉 + 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 { - 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 - } + + 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 } - prevReady = out.Ready prevHist = out.Histogram - if i > 1000 { // 安全閥 + + // 安全閥(理論上不會到) + if i > 2000 { break } } + if !foundFlip { t.Fatalf("expected at least one histogram sign flip (cross), but not found") } @@ -178,46 +191,3 @@ func TestMACD_ParamsValidation(t *testing.T) { _ = 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") - } -} diff --git a/internal/lib/strategy/ring_queue.go b/internal/lib/strategy/ring_queue.go deleted file mode 100644 index 72f2c8b..0000000 --- a/internal/lib/strategy/ring_queue.go +++ /dev/null @@ -1,55 +0,0 @@ -package strategy - -import ( - "container/list" - "github.com/shopspring/decimal" -) - -/************** 基礎的固定長度隊列,用來計算移動平均 **************/ -// 請注意: 目前為非併發版本,使用場警還不需要 - -type ringQD struct { - N uint // 窗口大小(需要保留的資料數量)需要保留的數量沒有負數 - l *list.List // 用於儲存資料的雙向鏈表 - sum decimal.Decimal // 當前窗口的總和,方便快速計算平均值 -} - -// 建立一個固定長度的隊列 -func newRingQD(n uint) *ringQD { - return &ringQD{N: n, l: list.New(), sum: decimal.Zero} -} - -// push:將新的數值放入隊列,並維護總和 -func (q *ringQD) push(x decimal.Decimal) { - q.l.PushBack(x) - q.sum = q.sum.Add(x) - // 如果超出最大長度,移除最舊的數值 - - if uint(q.l.Len()) > q.N { - f := q.l.Front() - q.sum = q.sum.Sub(f.Value.(decimal.Decimal)) - q.l.Remove(f) - } -} - -// ready:判斷隊列是否已經填滿 -func (q *ringQD) ready() bool { return q.N > 0 && uint(q.l.Len()) == q.N } - -// mean:計算平均值 -func (q *ringQD) mean() decimal.Decimal { - if q.l.Len() == 0 { - return decimal.Zero - } - - return q.sum.Div(decimal.NewFromInt(int64(q.l.Len()))) -} - -// values:返回隊列中所有的值(複製一份,不影響原資料) -func (q *ringQD) values() []decimal.Decimal { - out := make([]decimal.Decimal, 0, q.l.Len()) - for e := q.l.Front(); e != nil; e = e.Next() { - out = append(out, e.Value.(decimal.Decimal)) - } - - return out -} diff --git a/internal/lib/strategy/ring_queue_test.go b/internal/lib/strategy/ring_queue_test.go deleted file mode 100644 index 17215bf..0000000 --- a/internal/lib/strategy/ring_queue_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package strategy - -import ( - "github.com/shopspring/decimal" - "reflect" - "testing" -) - -// --- 表格式驅動測試 (Table-Driven Test) --- - -func TestRingQD(t *testing.T) { - // 為了方便測試,預先建立幾個 decimal 數值 - d10 := decimal.NewFromInt(10) - d20 := decimal.NewFromInt(20) - d30 := decimal.NewFromInt(30) - d40 := decimal.NewFromInt(40) - d50 := decimal.NewFromInt(50) - d_neg5 := decimal.NewFromInt(-5) - - // 定義測試案例的結構 - testCases := []struct { - name string // 測試案例的名稱 - n uint // ringQD 的大小 - inputs []decimal.Decimal // 輸入的數值序列 - wantSum decimal.Decimal // 預期的總和 - wantMean decimal.Decimal // 預期的平均值 - wantValues []decimal.Decimal // 預期隊列中最後的值 - wantReady bool // 預期的就緒狀態 - }{ - { - name: "未滿載的情況", - n: 5, - inputs: []decimal.Decimal{d10, d20}, - wantSum: decimal.NewFromInt(30), - wantMean: decimal.NewFromInt(15), - wantValues: []decimal.Decimal{d10, d20}, - wantReady: false, - }, - { - name: "剛好滿載的情況", - n: 3, - inputs: []decimal.Decimal{d10, d20, d30}, - wantSum: decimal.NewFromInt(60), - wantMean: decimal.NewFromInt(20), - wantValues: []decimal.Decimal{d10, d20, d30}, - wantReady: true, - }, - { - name: "超出容量,舊資料被移除", - n: 3, - inputs: []decimal.Decimal{d10, d20, d30, d40, d50}, - wantSum: decimal.NewFromInt(120), // 30 + 40 + 50 - wantMean: decimal.NewFromInt(40), - wantValues: []decimal.Decimal{d30, d40, d50}, - wantReady: true, - }, - { - name: "包含零與負數", - n: 4, - inputs: []decimal.Decimal{d10, d_neg5, decimal.Zero, d30, d_neg5}, - wantSum: decimal.NewFromInt(20), // -5 + 0 + 30 + (-5) - wantMean: decimal.NewFromInt(5), - wantValues: []decimal.Decimal{d_neg5, decimal.Zero, d30, d_neg5}, - wantReady: true, - }, - { - name: "初始為空", - n: 5, - inputs: []decimal.Decimal{}, - wantSum: decimal.Zero, - wantMean: decimal.Zero, - wantValues: []decimal.Decimal{}, - wantReady: false, - }, - { - name: "N 為 1 的邊界情況", - n: 1, - inputs: []decimal.Decimal{d10, d20, d30}, - wantSum: d30, - wantMean: d30, - wantValues: []decimal.Decimal{d30}, - wantReady: true, - }, - { - name: "N 為 0 的無效情況", - n: 0, - inputs: []decimal.Decimal{d10, d20, d30}, - wantSum: decimal.Zero, - wantMean: decimal.Zero, - wantValues: []decimal.Decimal{}, - wantReady: false, - }, - } - - // 遍歷所有測試案例 - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // 針對每個案例建立一個新的 ringQD - q := newRingQD(tc.n) - - // 依序推入資料 - for _, val := range tc.inputs { - q.push(val) - } - - // 驗證總和 - if !q.sum.Equals(tc.wantSum) { - t.Errorf("Sum 錯誤:got %v, want %v", q.sum, tc.wantSum) - } - - // 驗證平均值 - gotMean := q.mean() - if !gotMean.Equals(tc.wantMean) { - t.Errorf("Mean 錯誤:got %v, want %v", gotMean, tc.wantMean) - } - - // 驗證就緒狀態 - if q.ready() != tc.wantReady { - t.Errorf("Ready 狀態錯誤:got %v, want %v", q.ready(), tc.wantReady) - } - - // 驗證隊列中的值 - gotValues := q.values() - if !reflect.DeepEqual(gotValues, tc.wantValues) { - t.Errorf("Values 錯誤:got %v, want %v", gotValues, tc.wantValues) - } - }) - } -}