Files
exchange-monitor-go/config.go
T

238 lines
8.4 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)
// 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
}