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:
@@ -4,6 +4,8 @@
|
||||
// - Support / Resistance:pivot 局部极值 + 0.3% 价格聚类
|
||||
// - RangeHigh / RangeLow:近 N 根 P95(High) / P5(Low) 线性插值
|
||||
// - LongShortLine:近 N 根 LSR 穿越 1.0 的价位中位数
|
||||
// - Bollinger(per interval):SMA20 + 2σ
|
||||
// - Vegas(per interval):EMA12/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。
|
||||
// Trend:fast > mid > slow → bull;fast < 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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user