add sma ema macd

This commit is contained in:
王性驊 2025-08-17 22:54:16 +08:00
parent df849d98b7
commit 66b734350d
3 changed files with 35 additions and 249 deletions

View File

@ -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")
}
}

View File

@ -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
}

View File

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