package main import ( "encoding/json" "os" "strconv" "time" ) // 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 net profit % to trigger alert ScanIntervalMs int // how often scanner runs (milliseconds) // Automated trading TradeEnabled bool TradeThreshold float64 // minimum profit % to execute trade TradeAmountUSD float64 // amount per leg in USDT TradeCooldownMs int // ms between trades of same coin MaxPositions int // max concurrent open positions (0 = unlimited) // Capital InitialCapital float64 // starting capital in USD (for PnL % calculation) // Blacklist — stale spread observation BlacklistDuration time.Duration // how long a coin stays blacklisted (0 = permanent) // Test mode (no real API keys needed) TestMode bool MockSlippagePct float64 // simulated slippage per order (e.g. 0.01 = 0.01%) // Exchange fee rates (% per order) TakerFeeBitget float64 TakerFeeHyperLiquid float64 // Exit/risk parameters TakeProfitPct float64 // net profit % threshold for take-profit PositionTimeout time.Duration // max position hold time before auto-close LegDelay time.Duration // delay between placing long and short legs // Scale-in parameters ScaleStepPct float64 // spread widening % trigger for each scale level ScaleCooldown time.Duration // minimum time between scale-ins // Entry sanity check: reject if price moved beyond this % in the wrong direction ReversalTolerancePct float64 // Bitget API BitgetAPIKey string BitgetAPISecret string BitgetPassphrase string // HyperLiquid API HLPrivateKey string // ed25519 private key hex HLAddress string // main account address HLAPIAddress string // API wallet address (signer, auto-derived if empty) } // jsonConfig maps config.json fields (non-secret defaults checked into git). type jsonConfig struct { TestMode bool `json:"test_mode"` TradeEnabled bool `json:"trade_enabled"` ArbThreshold float64 `json:"arb_threshold"` ScanIntervalMs int `json:"scan_interval_ms"` TradeThreshold float64 `json:"trade_threshold"` TradeAmountUSD float64 `json:"trade_amount_usd"` TradeCooldownMs int `json:"trade_cooldown_ms"` AlertCooldownSec int `json:"alert_cooldown_sec"` MockSlippagePct float64 `json:"mock_slippage_pct"` MaxPositions int `json:"max_positions"` BlacklistDuration int `json:"blacklist_duration_sec"` InitialCapital float64 `json:"initial_capital"` // New: exchange fees TakerFeeBitget float64 `json:"taker_fee_bitget"` TakerFeeHyperLiquid float64 `json:"taker_fee_hyperliquid"` // New: exit/risk parameters TakeProfitPct float64 `json:"take_profit_pct"` PositionTimeoutSec int `json:"position_timeout_sec"` LegDelayMs int `json:"leg_delay_ms"` ReversalTolerancePct float64 `json:"reversal_tolerance_pct"` ScaleStepPct float64 `json:"scale_step_pct"` ScaleCooldownSec int `json:"scale_cooldown_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))), TradeEnabled: getBool("TRADE_ENABLED", jsonCfg.TradeEnabled), TradeThreshold: getFloat("TRADE_THRESHOLD", jsonCfg.TradeThreshold), TradeAmountUSD: getFloat("TRADE_AMOUNT_USD", jsonCfg.TradeAmountUSD), TradeCooldownMs: int(getFloat("TRADE_COOLDOWN_MS", float64(jsonCfg.TradeCooldownMs))), MaxPositions: int(getFloat("MAX_POSITIONS", float64(jsonCfg.MaxPositions))), InitialCapital: getFloat("INITIAL_CAPITAL", jsonCfg.InitialCapital), BlacklistDuration: time.Duration(getFloat("BLACKLIST_DURATION_SEC", float64(jsonCfg.BlacklistDuration))) * time.Second, TestMode: getBool("TEST_MODE", jsonCfg.TestMode), MockSlippagePct: getFloat("MOCK_SLIPPAGE_PCT", jsonCfg.MockSlippagePct), // Exchange fee rates TakerFeeBitget: getFloat("TAKER_FEE_BITGET", jsonCfg.TakerFeeBitget), TakerFeeHyperLiquid: getFloat("TAKER_FEE_HYPERLIQUID", jsonCfg.TakerFeeHyperLiquid), // Exit/risk parameters TakeProfitPct: getFloat("TAKE_PROFIT_PCT", jsonCfg.TakeProfitPct), PositionTimeout: time.Duration(getFloat("POSITION_TIMEOUT_SEC", float64(jsonCfg.PositionTimeoutSec))) * time.Second, LegDelay: time.Duration(getFloat("LEG_DELAY_MS", float64(jsonCfg.LegDelayMs))) * time.Millisecond, ReversalTolerancePct: getFloat("REVERSAL_TOLERANCE_PCT", jsonCfg.ReversalTolerancePct), // Scale-in parameters ScaleStepPct: getFloat("SCALE_STEP_PCT", jsonCfg.ScaleStepPct), ScaleCooldown: time.Duration(getFloat("SCALE_COOLDOWN_SEC", float64(jsonCfg.ScaleCooldownSec))) * time.Second, BitgetAPIKey: getEnv("BITGET_API_KEY", ""), BitgetAPISecret: getEnv("BITGET_API_SECRET", ""), BitgetPassphrase: getEnv("BITGET_PASSPHRASE", ""), HLPrivateKey: getEnv("HL_PRIVATE_KEY", ""), HLAddress: getEnv("HL_ADDRESS", ""), HLAPIAddress: getEnv("HL_API_ADDRESS", ""), } } func loadJSONConfig() jsonConfig { def := jsonConfig{ ArbThreshold: 0.03, ScanIntervalMs: 500, TradeThreshold: 0.15, TradeAmountUSD: 10, TradeCooldownMs: 30000, AlertCooldownSec: 300, MockSlippagePct: 0.005, MaxPositions: 5, // default max 5 concurrent positions BlacklistDuration: 3600, // default 1 hour blacklist observation InitialCapital: 1000, // default $1000 starting capital // Exchange fee rates TakerFeeBitget: 0.060, // 0.060% TakerFeeHyperLiquid: 0.045, // 0.045% // Exit/risk parameters TakeProfitPct: 0.20, // 0.20% net profit take-profit PositionTimeoutSec: 1800, // 30 minutes LegDelayMs: 300, // 300ms between legs ReversalTolerancePct: 0.1, // 0.1% tolerance for entry sanity check // Scale-in parameters ScaleStepPct: 0.10, // 0.10% spread widening per scale level ScaleCooldownSec: 5, // 5 seconds between scales } 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.TradeThreshold != 0 { def.TradeThreshold = cfg.TradeThreshold } if cfg.TradeAmountUSD != 0 { def.TradeAmountUSD = cfg.TradeAmountUSD } if cfg.TradeCooldownMs != 0 { def.TradeCooldownMs = cfg.TradeCooldownMs } if cfg.AlertCooldownSec != 0 { def.AlertCooldownSec = cfg.AlertCooldownSec } if cfg.MockSlippagePct != 0 { def.MockSlippagePct = cfg.MockSlippagePct } if cfg.MaxPositions != 0 { def.MaxPositions = cfg.MaxPositions } if cfg.BlacklistDuration != 0 { def.BlacklistDuration = cfg.BlacklistDuration } if cfg.InitialCapital != 0 { def.InitialCapital = cfg.InitialCapital } // New config fields if cfg.TakerFeeBitget != 0 { def.TakerFeeBitget = cfg.TakerFeeBitget } if cfg.TakerFeeHyperLiquid != 0 { def.TakerFeeHyperLiquid = cfg.TakerFeeHyperLiquid } if cfg.TakeProfitPct != 0 { def.TakeProfitPct = cfg.TakeProfitPct } if cfg.PositionTimeoutSec != 0 { def.PositionTimeoutSec = cfg.PositionTimeoutSec } if cfg.LegDelayMs != 0 { def.LegDelayMs = cfg.LegDelayMs } if cfg.ReversalTolerancePct != 0 { def.ReversalTolerancePct = cfg.ReversalTolerancePct } if cfg.ScaleStepPct != 0 { def.ScaleStepPct = cfg.ScaleStepPct } if cfg.ScaleCooldownSec != 0 { def.ScaleCooldownSec = cfg.ScaleCooldownSec } // Boolean fields: zero default is false, so use OR logic // When JSON has true → true || false = true (override) // When JSON has false → false || false = false (keep default) def.TestMode = cfg.TestMode || def.TestMode def.TradeEnabled = cfg.TradeEnabled || def.TradeEnabled return def }