package config import ( "fmt" "os" "strings" "time" "github.com/spf13/viper" ) type Config struct { Server ServerConfig `mapstructure:"server"` Database DatabaseConfig `mapstructure:"database"` Redis RedisConfig `mapstructure:"redis"` SMTP SMTPConfig `mapstructure:"smtp"` RateLimit RateLimitConfig `mapstructure:"rate_limit"` Logbull LogbullConfig `mapstructure:"logbull"` Subscriptions []SubscriptionConfig `mapstructure:"subscriptions"` SubscriptionDedupTTL time.Duration `mapstructure:"subscription_dedup_ttl"` } type SubscriptionConfig struct { Name string `mapstructure:"name"` URL string `mapstructure:"url"` Queue string `mapstructure:"queue"` DeadLetterQueue string `mapstructure:"dead_letter_queue"` Exchange string `mapstructure:"exchange"` ExchangeType string `mapstructure:"exchange_type"` RoutingKey string `mapstructure:"routing_key"` MaxRetry int `mapstructure:"max_retry"` Source string `mapstructure:"source"` Formatter string `mapstructure:"formatter"` StrategyOverrides map[string]StrategyOverride `mapstructure:"strategy_overrides"` } type StrategyOverride struct { QuantityMultipliers QuantityMultipliers `mapstructure:"quantity_multipliers"` Leverage *int `mapstructure:"leverage"` } type QuantityMultipliers struct { Open float64 `mapstructure:"open"` Add float64 `mapstructure:"add"` Reduce float64 `mapstructure:"reduce"` Close float64 `mapstructure:"close"` } func (o StrategyOverride) QuantityMultiplierFor(action string) float64 { var v float64 switch strings.ToUpper(action) { case "OPEN": v = o.QuantityMultipliers.Open case "ADD": v = o.QuantityMultipliers.Add case "REDUCE": v = o.QuantityMultipliers.Reduce case "CLOSE": v = o.QuantityMultipliers.Close default: return 1 } if v <= 0 { return 1 } return v } func (c *Config) NormalizeSubscriptions() error { for i := range c.Subscriptions { s := &c.Subscriptions[i] if s.URL == "" { continue } if s.Queue == "" { return fmt.Errorf("subscriptions[%d]: queue is required", i) } if s.Source == "" { return fmt.Errorf("subscriptions[%d]: source is required", i) } if s.Name == "" { s.Name = s.Queue } if s.MaxRetry <= 0 { s.MaxRetry = 3 } if s.ExchangeType == "" { s.ExchangeType = "fanout" } if s.Formatter == "" { s.Formatter = "trade_signal" } if s.Formatter != "trade_signal" { return fmt.Errorf("subscriptions[%d]: unknown formatter %q", i, s.Formatter) } } return nil } func (c *Config) ActiveSubscriptions() []SubscriptionConfig { out := make([]SubscriptionConfig, 0, len(c.Subscriptions)) for _, s := range c.Subscriptions { if s.URL != "" { out = append(out, s) } } return out } type ServerConfig struct { Port int `mapstructure:"port"` AdminKey string `mapstructure:"admin_key"` } type DatabaseConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` Database string `mapstructure:"database"` } func (d DatabaseConfig) DSN() string { return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local", d.User, d.Password, d.Host, d.Port, d.Database) } type RedisConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` Password string `mapstructure:"password"` DB int `mapstructure:"db"` } func (r RedisConfig) Addr() string { return fmt.Sprintf("%s:%d", r.Host, r.Port) } type SMTPConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` User string `mapstructure:"user"` Password string `mapstructure:"password"` From string `mapstructure:"from"` } type RateLimitConfig struct { Default int `mapstructure:"default"` DingTalkPerMin int `mapstructure:"dingtalk_per_min"` // per robot webhook; 0 => 18 } type LogbullConfig struct { Host string `mapstructure:"host"` ProjectID string `mapstructure:"project_id"` APIKey string `mapstructure:"api_key"` LogLevel string `mapstructure:"log_level"` } func Load(path string) (*Config, error) { v := viper.New() v.SetConfigFile(path) v.SetEnvPrefix("NOTIFY") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("read config: %w", err) } // Expand ${ENV_VAR} placeholders in config values for _, key := range v.AllKeys() { val := v.GetString(key) if strings.Contains(val, "${") { expanded := expandEnv(val) v.Set(key, expanded) } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("unmarshal config: %w", err) } if err := cfg.NormalizeSubscriptions(); err != nil { return nil, err } if cfg.SubscriptionDedupTTL <= 0 { cfg.SubscriptionDedupTTL = time.Hour } return &cfg, nil } func expandEnv(s string) string { return os.Expand(s, func(key string) string { // support ${VAR:-default} if i := strings.Index(key, ":-"); i >= 0 { name := key[:i] def := key[i+2:] if v, ok := os.LookupEnv(name); ok { return v } return def } return os.Getenv(key) }) }