删除 HyperLiquid + 全部交易功能,构建自适应 surge 检测器。 - 新增 surge_detector.go: 每币独立滚动窗口基线,检测三所价差异常飙升 - 新增 SpreadCard/SurgeCard 前端组件 - 保留 momentum/trend/cumulative/trend_filter 扫描功能 - 更新文档和配置以反映新系统 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
6.6 KiB
Go
202 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"strconv"
|
|
)
|
|
|
|
// Config holds all system configuration.
|
|
// Priority: .env vars > config.json > code defaults.
|
|
type Config struct {
|
|
TelegramBotToken string
|
|
TelegramChatID string
|
|
AlertCooldownSec int // seconds between alerts for same coin
|
|
ArbThreshold float64 // minimum spread % to trigger alert
|
|
ScanIntervalMs int // how often scanner runs (milliseconds)
|
|
|
|
// Surge detection
|
|
SurgeEnabled bool
|
|
SurgeWindowSize int // rolling window samples (default: 600 = ~30s)
|
|
SurgeBaselineMultiplier float64 // baseline * N = threshold (default: 3.0)
|
|
SurgeMinAbsSpreadPct float64 // minimum absolute spread % (default: 0.05)
|
|
SurgeCooldownSec int // cooldown seconds per coin (default: 60)
|
|
|
|
// Momentum scanning mode
|
|
MomentumEnabled bool
|
|
MomentumThresholdPct float64
|
|
|
|
// Trend detection mode
|
|
TrendEnabled bool
|
|
TrendBaselineWindow int // ticks for EMA volatility baseline (default: 600 = 30s)
|
|
TrendAnomalyMul float64 // z-score multiplier for alert threshold (default: 3.0)
|
|
TrendConfirmTicks int // ticks needed for state confirmation (default: 3)
|
|
TrendAlertCooldown int64 // ms cooldown between alerts for same coin (default: 60000)
|
|
}
|
|
|
|
// jsonConfig maps config.json fields (non-secret defaults checked into git).
|
|
type jsonConfig struct {
|
|
ArbThreshold float64 `json:"arb_threshold"`
|
|
ScanIntervalMs int `json:"scan_interval_ms"`
|
|
AlertCooldownSec int `json:"alert_cooldown_sec"`
|
|
|
|
// Surge detection
|
|
SurgeEnabled bool `json:"surge_enabled"`
|
|
SurgeWindowSize int `json:"surge_window_size"`
|
|
SurgeBaselineMultiplier float64 `json:"surge_baseline_multiplier"`
|
|
SurgeMinAbsSpreadPct float64 `json:"surge_min_abs_spread_pct"`
|
|
SurgeCooldownSec int `json:"surge_cooldown_sec"`
|
|
|
|
// Momentum scanning
|
|
MomentumEnabled bool `json:"momentum_enabled"`
|
|
MomentumThresholdPct float64 `json:"momentum_threshold_pct"`
|
|
|
|
// Trend detection
|
|
TrendEnabled bool `json:"trend_enabled"`
|
|
TrendBaselineWindow int `json:"trend_baseline_window"`
|
|
TrendAnomalyMul float64 `json:"trend_anomaly_mul"`
|
|
TrendConfirmTicks int `json:"trend_confirm_ticks"`
|
|
TrendAlertCooldown int64 `json:"trend_alert_cooldown_ms"`
|
|
}
|
|
|
|
func LoadConfig() *Config {
|
|
// 1. Load config.json defaults
|
|
jsonCfg := loadJSONConfig()
|
|
|
|
// 2. .env vars override config.json
|
|
getEnv := func(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
getFloat := func(key string, def float64) float64 {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
f, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return f
|
|
}
|
|
getBool := func(key string, def bool) bool {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
return v == "1" || v == "true" || v == "yes"
|
|
}
|
|
|
|
return &Config{
|
|
TelegramBotToken: getEnv("TELEGRAM_BOT_TOKEN", ""),
|
|
TelegramChatID: getEnv("TELEGRAM_CHAT_ID", ""),
|
|
AlertCooldownSec: int(getFloat("ALERT_COOLDOWN_SEC", float64(jsonCfg.AlertCooldownSec))),
|
|
ArbThreshold: getFloat("ARB_THRESHOLD", jsonCfg.ArbThreshold),
|
|
ScanIntervalMs: int(getFloat("SCAN_INTERVAL_MS", float64(jsonCfg.ScanIntervalMs))),
|
|
|
|
// Surge detection
|
|
SurgeEnabled: getBool("SURGE_ENABLED", jsonCfg.SurgeEnabled),
|
|
SurgeWindowSize: int(getFloat("SURGE_WINDOW_SIZE", float64(jsonCfg.SurgeWindowSize))),
|
|
SurgeBaselineMultiplier: getFloat("SURGE_BASELINE_MULTIPLIER", jsonCfg.SurgeBaselineMultiplier),
|
|
SurgeMinAbsSpreadPct: getFloat("SURGE_MIN_ABS_SPREAD_PCT", jsonCfg.SurgeMinAbsSpreadPct),
|
|
SurgeCooldownSec: int(getFloat("SURGE_COOLDOWN_SEC", float64(jsonCfg.SurgeCooldownSec))),
|
|
|
|
// Momentum scanning
|
|
MomentumEnabled: getBool("MOMENTUM_ENABLED", jsonCfg.MomentumEnabled),
|
|
MomentumThresholdPct: getFloat("MOMENTUM_THRESHOLD_PCT", jsonCfg.MomentumThresholdPct),
|
|
|
|
// Trend detection
|
|
TrendEnabled: getBool("TREND_ENABLED", jsonCfg.TrendEnabled),
|
|
TrendBaselineWindow: int(getFloat("TREND_BASELINE_WINDOW", float64(jsonCfg.TrendBaselineWindow))),
|
|
TrendAnomalyMul: getFloat("TREND_ANOMALY_MUL", jsonCfg.TrendAnomalyMul),
|
|
TrendConfirmTicks: int(getFloat("TREND_CONFIRM_TICKS", float64(jsonCfg.TrendConfirmTicks))),
|
|
TrendAlertCooldown: int64(getFloat("TREND_ALERT_COOLDOWN_MS", float64(jsonCfg.TrendAlertCooldown))),
|
|
}
|
|
}
|
|
|
|
func loadJSONConfig() jsonConfig {
|
|
def := jsonConfig{
|
|
ArbThreshold: 0.03,
|
|
ScanIntervalMs: 500,
|
|
AlertCooldownSec: 300,
|
|
|
|
// Surge detection
|
|
SurgeEnabled: true,
|
|
SurgeWindowSize: 600, // ~30s at 50ms tick
|
|
SurgeBaselineMultiplier: 3.0, // baseline * N = threshold
|
|
SurgeMinAbsSpreadPct: 0.05, // minimum absolute spread %
|
|
SurgeCooldownSec: 60, // seconds between alerts for same coin
|
|
|
|
// Momentum scanning
|
|
MomentumThresholdPct: 0.25, // 0.25% change flags momentum
|
|
|
|
// Trend detection
|
|
TrendBaselineWindow: 600, // ~30s at 50ms tick
|
|
TrendAnomalyMul: 3.0, // 3 sigma z-score threshold
|
|
TrendConfirmTicks: 3, // 3 consecutive ticks for confirmation
|
|
TrendAlertCooldown: 60000, // 1 min cooldown
|
|
}
|
|
|
|
data, err := os.ReadFile("config.json")
|
|
if err != nil {
|
|
return def // file not found, use code defaults
|
|
}
|
|
|
|
var cfg jsonConfig
|
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
|
return def
|
|
}
|
|
|
|
// Only override if the JSON file actually set the field
|
|
if cfg.ArbThreshold != 0 {
|
|
def.ArbThreshold = cfg.ArbThreshold
|
|
}
|
|
if cfg.ScanIntervalMs != 0 {
|
|
def.ScanIntervalMs = cfg.ScanIntervalMs
|
|
}
|
|
if cfg.AlertCooldownSec != 0 {
|
|
def.AlertCooldownSec = cfg.AlertCooldownSec
|
|
}
|
|
|
|
// Surge detection JSON overrides
|
|
if cfg.SurgeWindowSize != 0 {
|
|
def.SurgeWindowSize = cfg.SurgeWindowSize
|
|
}
|
|
if cfg.SurgeBaselineMultiplier != 0 {
|
|
def.SurgeBaselineMultiplier = cfg.SurgeBaselineMultiplier
|
|
}
|
|
if cfg.SurgeMinAbsSpreadPct != 0 {
|
|
def.SurgeMinAbsSpreadPct = cfg.SurgeMinAbsSpreadPct
|
|
}
|
|
if cfg.SurgeCooldownSec != 0 {
|
|
def.SurgeCooldownSec = cfg.SurgeCooldownSec
|
|
}
|
|
|
|
if cfg.MomentumThresholdPct != 0 {
|
|
def.MomentumThresholdPct = cfg.MomentumThresholdPct
|
|
}
|
|
|
|
// Trend detection JSON overrides
|
|
if cfg.TrendBaselineWindow != 0 {
|
|
def.TrendBaselineWindow = cfg.TrendBaselineWindow
|
|
}
|
|
if cfg.TrendAnomalyMul != 0 {
|
|
def.TrendAnomalyMul = cfg.TrendAnomalyMul
|
|
}
|
|
if cfg.TrendConfirmTicks != 0 {
|
|
def.TrendConfirmTicks = cfg.TrendConfirmTicks
|
|
}
|
|
if cfg.TrendAlertCooldown != 0 {
|
|
def.TrendAlertCooldown = cfg.TrendAlertCooldown
|
|
}
|
|
|
|
// Boolean fields: zero default is false, so use OR logic
|
|
def.MomentumEnabled = cfg.MomentumEnabled || def.MomentumEnabled
|
|
def.TrendEnabled = cfg.TrendEnabled || def.TrendEnabled
|
|
def.SurgeEnabled = cfg.SurgeEnabled || def.SurgeEnabled
|
|
|
|
return def
|
|
}
|