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

@@ -4,6 +4,8 @@
// - Support / Resistancepivot 局部极值 + 0.3% 价格聚类
// - RangeHigh / RangeLow近 N 根 P95(High) / P5(Low) 线性插值
// - LongShortLine近 N 根 LSR 穿越 1.0 的价位中位数
// - Bollingerper intervalSMA20 + 2σ
// - Vegasper intervalEMA12/144/169 三线 + 趋势判定
//
// 数值边界:本文件内部允许 transient float64但输出到 entity 前
// 必须 FormatFloat 转回 string。详见 ai/adr/0002-indicator-numeric-boundary.md
@@ -38,6 +40,17 @@ const (
// TechnicalLevel.Source 值
sourceSupportPivot = "pivot_low"
sourceResistancePivot = "pivot_high"
// Bollinger周期 20、倍数 2、Squeeze 阈值按 BandwidthPct百分比
bbPeriod = 20
bbStdMult = 2.0
bbSqueezeTight = 4.0
bbSqueezeExpanded = 12.0
// Vegas 通道 EMA 周期
vegasFast = 12
vegasMid = 144
vegasSlow = 169
)
// IndicatorUsecase 实现 IndicatorComputer 接口。
@@ -54,14 +67,18 @@ type pivotPoint struct {
}
// Compute 是 IndicatorComputer 的唯一对外方法。
// klines 必须按 OpenTime 升序排好KlineRepository.FindRecent 已保证)。
// klinesByInterval 每个 value 必须按 OpenTime 升序排好KlineRepository.FindRecent 已保证)。
// primaryInterval 指定 support/resistance/range/longShortLine 用哪个周期;
// 其它周期(包括 primary 自己)都会参与 Intervals 里的 Bollinger / Vegas。
// longShort 同样升序;可能为空。
func (u *IndicatorUsecase) Compute(
klines []entity.Kline,
klinesByInterval map[string][]entity.Kline,
primaryInterval string,
longShort []entity.LongShortRatio,
) entity.TechnicalStructure {
highs := pivotHighs(klines, pivotLeft, pivotRight)
lows := pivotLows(klines, pivotLeft, pivotRight)
primary := klinesByInterval[primaryInterval]
highs := pivotHighs(primary, pivotLeft, pivotRight)
lows := pivotLows(primary, pivotLeft, pivotRight)
resistance := clusterLevels(highs, clusterPctThreshold, sourceResistancePivot)
support := clusterLevels(lows, clusterPctThreshold, sourceSupportPivot)
if support == nil {
@@ -70,17 +87,45 @@ func (u *IndicatorUsecase) Compute(
if resistance == nil {
resistance = []entity.TechnicalLevel{}
}
hi, lo := rangeHighLow(klines)
lsLine := longShortCrossings(longShort, klines, longShortCrossKeep)
hi, lo := rangeHighLow(primary)
lsLine := longShortCrossings(longShort, primary, longShortCrossKeep)
intervals := make(map[string]entity.IntervalTechnicals, len(klinesByInterval))
for iv, ks := range klinesByInterval {
intervals[iv] = computeIntervalTechnicals(ks)
}
return entity.TechnicalStructure{
Support: support,
Resistance: resistance,
RangeHigh: hi,
RangeLow: lo,
LongShortLine: lsLine,
Intervals: intervals,
}
}
// computeIntervalTechnicals 在单周期 K 线上算 Bollinger 与 Vegas。
// 任一项数据不足则对应字段为 nil。
func computeIntervalTechnicals(klines []entity.Kline) entity.IntervalTechnicals {
closes := parseCloses(klines)
return entity.IntervalTechnicals{
Bollinger: bollinger(closes),
Vegas: vegas(closes),
}
}
// parseCloses 提取 close 价位,跳过解析失败的根。
func parseCloses(klines []entity.Kline) []float64 {
out := make([]float64, 0, len(klines))
for _, k := range klines {
if v, ok := parsePrice(k.Close); ok {
out = append(out, v)
}
}
return out
}
// parsePrice 用 strconv 把 string 价格解析成 float64。
// 失败(空 / 非数字 / 含空格)返回 ok=false调用者应跳过该值不 panic。
// NaN 与 ±Inf 也视为解析失败。
@@ -377,3 +422,111 @@ func medianFloat(xs []float64) float64 {
}
return (xs[n/2-1] + xs[n/2]) / 2
}
// sma 计算切片末尾 period 个值的简单移动均值。
// len < period 返回 ok=false。
func sma(values []float64, period int) (float64, bool) {
n := len(values)
if period <= 0 || n < period {
return 0, false
}
sum := 0.0
for _, v := range values[n-period:] {
sum += v
}
return sum / float64(period), true
}
// stddevPop 计算切片末尾 period 个值相对 mean 的总体标准差(除以 N
// 金融惯例TradingView / TA-Lib布林带用 population stddev不用 sample。
func stddevPop(values []float64, period int, mean float64) (float64, bool) {
n := len(values)
if period <= 0 || n < period {
return 0, false
}
sumSq := 0.0
for _, v := range values[n-period:] {
d := v - mean
sumSq += d * d
}
return math.Sqrt(sumSq / float64(period)), true
}
// emaLast 计算时间序列末尾的 EMA(period)。
// 初值用前 period 根的 SMA标准做法之后用 α=2/(period+1) 递推。
// len < period 返回 ok=false。
func emaLast(values []float64, period int) (float64, bool) {
n := len(values)
if period <= 0 || n < period {
return 0, false
}
seed := 0.0
for _, v := range values[:period] {
seed += v
}
ema := seed / float64(period)
if n == period {
return ema, true
}
alpha := 2.0 / float64(period+1)
for _, v := range values[period:] {
ema = alpha*v + (1-alpha)*ema
}
return ema, true
}
// bollinger 在 closes 上算 20 周期、2σ 布林带。
// 数据不足 bbPeriod 根或 mid 为 0 时返回 nil。
// Squeeze 按 BandwidthPct百分比分档< 4 tight> 12 expanded否则 normal。
func bollinger(closes []float64) *entity.Bollinger {
mid, ok := sma(closes, bbPeriod)
if !ok || mid == 0 {
return nil
}
sd, ok := stddevPop(closes, bbPeriod, mid)
if !ok {
return nil
}
upper := mid + bbStdMult*sd
lower := mid - bbStdMult*sd
bw := (upper - lower) / mid * 100
squeeze := "normal"
switch {
case bw < bbSqueezeTight:
squeeze = "tight"
case bw > bbSqueezeExpanded:
squeeze = "expanded"
}
return &entity.Bollinger{
Mid: formatPrice(mid),
Upper: formatPrice(upper),
Lower: formatPrice(lower),
BandwidthPct: formatPrice(bw),
Squeeze: squeeze,
}
}
// vegas 在 closes 上算 EMA12/144/169 + 趋势判定。
// 任一 EMA 计算失败(长度不足 169则返回 nil。
// Trendfast > mid > slow → bullfast < mid < slow → bear其余 range。
func vegas(closes []float64) *entity.Vegas {
fast, ok1 := emaLast(closes, vegasFast)
mid, ok2 := emaLast(closes, vegasMid)
slow, ok3 := emaLast(closes, vegasSlow)
if !ok1 || !ok2 || !ok3 {
return nil
}
trend := "range"
switch {
case fast > mid && mid > slow:
trend = "bull"
case fast < mid && mid < slow:
trend = "bear"
}
return &entity.Vegas{
EMA12: formatPrice(fast),
EMA144: formatPrice(mid),
EMA169: formatPrice(slow),
Trend: trend,
}
}