feat(market): Phase 1 扩展 — 更多 symbol + Bollinger/Vegas + 衍生品信号

- collector.symbols 与 supportedSymbols 加 SOL/BNB/DOGE,env-default 与 README 同步
- entity 追加 TechnicalStructure.Intervals(每周期 Bollinger + Vegas,omitempty 不破坏既有字段)与 DerivativesBundle.Signal
- 新增纯解析 usecase derivatives_signal.go:fundingBias 绝对阈值、oiSignal 用 P20 baseline + 价格方向(OI up + 价格 up/down → long/short_building,funding 不参与方向)、lsrRegime 绝对阈值;signal 数据不足走 warnings 进 DataQuality
- indicator.go 加 sma/stddev/ema/bollinger/vegas + computeIntervalTechnicals;IndicatorComputer 签名收 klines map
- harness 同步:G12 扩入 derivatives_signal.go(含 Makefile G12.5/6 + ADR-0002 适用范围 + project-map / harness-health 更新)
This commit is contained in:
dela
2026-05-24 23:19:57 +08:00
parent 622ff1d317
commit fa769331c2
16 changed files with 912 additions and 66 deletions

View File

@@ -491,7 +491,7 @@ func TestNearestKlineIndex(t *testing.T) {
func TestCompute_EmptyInputs(t *testing.T) {
u := NewIndicatorUsecase()
out := u.Compute(nil, nil)
out := u.Compute(nil, "1h", nil)
require.NotNil(t, out.Support)
require.NotNil(t, out.Resistance)
require.Empty(t, out.Support)
@@ -499,6 +499,8 @@ func TestCompute_EmptyInputs(t *testing.T) {
require.Nil(t, out.RangeHigh)
require.Nil(t, out.RangeLow)
require.Nil(t, out.LongShortLine)
require.NotNil(t, out.Intervals)
require.Empty(t, out.Intervals)
}
func TestCompute_SmokeWithVShape(t *testing.T) {
@@ -519,10 +521,113 @@ func TestCompute_SmokeWithVShape(t *testing.T) {
strconv.FormatFloat(p+0.25, 'f', -1, 64),
)
}
out := u.Compute(klines, nil)
out := u.Compute(map[string][]entity.Kline{"1h": klines}, "1h", nil)
require.Len(t, out.Support, 1)
require.Empty(t, out.Resistance)
require.NotNil(t, out.RangeHigh)
require.NotNil(t, out.RangeLow)
require.Nil(t, out.LongShortLine, "no LSR data → nil")
require.Contains(t, out.Intervals, "1h")
}
// ---- Bollinger / Vegas -------------------------------------------------
// flatCloses 生成 n 根价位恒为 v 的 closes用于 Squeeze=tight 验证)。
func flatCloses(n int, v float64) []float64 {
out := make([]float64, n)
for i := range out {
out[i] = v
}
return out
}
// ascCloses 生成等差递增 closesbase, base+step, ...
func ascCloses(n int, base, step float64) []float64 {
out := make([]float64, n)
for i := range out {
out[i] = base + float64(i)*step
}
return out
}
func TestBollinger_FlatSeries_Tight(t *testing.T) {
bb := bollinger(flatCloses(20, 100))
require.NotNil(t, bb)
require.Equal(t, "100", bb.Mid)
require.Equal(t, "100", bb.Upper)
require.Equal(t, "100", bb.Lower)
require.Equal(t, "0", bb.BandwidthPct)
require.Equal(t, "tight", bb.Squeeze)
}
func TestBollinger_AscendingSeries_Expanded(t *testing.T) {
// 0..29 等差,末 20 根 mid=19.5、σ ≈ 5.77、bw ≈ 118%
bb := bollinger(ascCloses(30, 0, 1))
require.NotNil(t, bb)
require.Equal(t, "expanded", bb.Squeeze)
mid, _ := strconv.ParseFloat(bb.Mid, 64)
require.InDelta(t, 19.5, mid, 1e-9)
}
func TestBollinger_InsufficientData(t *testing.T) {
require.Nil(t, bollinger(flatCloses(19, 100)))
require.Nil(t, bollinger(nil))
}
func TestVegas_BullishStack(t *testing.T) {
// 上升序列 → EMA12 > EMA144 > EMA169趋势 bull
v := vegas(ascCloses(200, 100, 1))
require.NotNil(t, v)
e12, _ := strconv.ParseFloat(v.EMA12, 64)
e144, _ := strconv.ParseFloat(v.EMA144, 64)
e169, _ := strconv.ParseFloat(v.EMA169, 64)
require.Greater(t, e12, e144)
require.Greater(t, e144, e169)
require.Equal(t, "bull", v.Trend)
}
func TestVegas_BearishStack(t *testing.T) {
// 下降序列
v := vegas(ascCloses(200, 300, -1))
require.NotNil(t, v)
require.Equal(t, "bear", v.Trend)
}
func TestVegas_FlatRange(t *testing.T) {
// 平盘:三条线相等
v := vegas(flatCloses(200, 100))
require.NotNil(t, v)
require.Equal(t, "range", v.Trend)
}
func TestVegas_InsufficientData(t *testing.T) {
require.Nil(t, vegas(ascCloses(168, 100, 1)))
}
func TestCompute_IntervalsPopulated(t *testing.T) {
u := NewIndicatorUsecase()
// 给 1h25 根) 与 4h200 根) 喂上升序列
mk := func(n int) []entity.Kline {
out := make([]entity.Kline, n)
for i := 0; i < n; i++ {
p := 100 + float64(i)
out[i] = mkKline(int64(i),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(p+0.5, 'f', -1, 64),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(p+0.25, 'f', -1, 64),
)
}
return out
}
out := u.Compute(map[string][]entity.Kline{
"1h": mk(25),
"4h": mk(200),
}, "1h", nil)
require.Contains(t, out.Intervals, "1h")
require.Contains(t, out.Intervals, "4h")
require.NotNil(t, out.Intervals["1h"].Bollinger, "25 closes 够算 BB")
require.Nil(t, out.Intervals["1h"].Vegas, "25 closes 不够算 EMA169")
require.NotNil(t, out.Intervals["4h"].Bollinger)
require.NotNil(t, out.Intervals["4h"].Vegas)
}