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) // Binance momentum detection (1-minute Binance-only price change) BinanceMomentumEnabled bool BinanceMomentumThresholdPct float64 // default: 10.0 BinanceMomentumCooldownSec int // default: 300 (5 min) BinanceMomentumWindowSec int // default: 60 } // 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"` // Binance momentum detection BinanceMomentumEnabled bool `json:"binance_momentum_enabled"` BinanceMomentumThresholdPct float64 `json:"binance_momentum_threshold_pct"` BinanceMomentumCooldownSec int `json:"binance_momentum_cooldown_sec"` BinanceMomentumWindowSec int `json:"binance_momentum_window_sec"` } 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))), // Binance momentum detection BinanceMomentumEnabled: getBool("BINANCE_MOMENTUM_ENABLED", jsonCfg.BinanceMomentumEnabled), BinanceMomentumThresholdPct: getFloat("BINANCE_MOMENTUM_THRESHOLD_PCT", jsonCfg.BinanceMomentumThresholdPct), BinanceMomentumCooldownSec: int(getFloat("BINANCE_MOMENTUM_COOLDOWN_SEC", float64(jsonCfg.BinanceMomentumCooldownSec))), BinanceMomentumWindowSec: int(getFloat("BINANCE_MOMENTUM_WINDOW_SEC", float64(jsonCfg.BinanceMomentumWindowSec))), } } 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 // Binance momentum detection BinanceMomentumEnabled: true, BinanceMomentumThresholdPct: 10.0, BinanceMomentumCooldownSec: 300, // 5 minutes between alerts per coin BinanceMomentumWindowSec: 60, // 1-minute lookback } 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 } // Binance momentum JSON overrides if cfg.BinanceMomentumThresholdPct != 0 { def.BinanceMomentumThresholdPct = cfg.BinanceMomentumThresholdPct } if cfg.BinanceMomentumCooldownSec != 0 { def.BinanceMomentumCooldownSec = cfg.BinanceMomentumCooldownSec } if cfg.BinanceMomentumWindowSec != 0 { def.BinanceMomentumWindowSec = cfg.BinanceMomentumWindowSec } // 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 def.BinanceMomentumEnabled = cfg.BinanceMomentumEnabled || def.BinanceMomentumEnabled return def }