- 新增OKX WebSocket行情连接器,扩展4交易所价格监控 - 新增z-score趋势检测引擎(TrendDetector),识别价格异动/趋势启动 - 新增累积变动跟踪(CumulativeTracker),基于1min/5min多交易所共识 - 趋势事件和累积变动事件持久化到SQLite - 新增Binance/OKX动量检测字段,扩展前端动量卡片至15列 - 迁移至macOS(darwin-arm64),更新前端依赖 - Dashboard网格重构:非交易卡片置顶,交易卡片置底 - TrackedCoin添加OK字段,添加ExBinance/ExOKX常量 - 前端新增趋势检测卡片、趋势历史卡片、累积变动卡片 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
340 lines
12 KiB
Go
340 lines
12 KiB
Go
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)
|
|
|
|
// ExcludedCoins — coins to never trade (hard block)
|
|
ExcludedCoins []string
|
|
|
|
// 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
|
|
|
|
// 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)
|
|
|
|
// 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"`
|
|
ExcludedCoins []string `json:"excluded_coins"`
|
|
|
|
// 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"`
|
|
|
|
// 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,
|
|
|
|
ExcludedCoins: jsonCfg.ExcludedCoins,
|
|
|
|
// 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))),
|
|
|
|
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
|
|
|
|
// 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.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
|
|
}
|
|
if len(cfg.ExcludedCoins) > 0 {
|
|
def.ExcludedCoins = cfg.ExcludedCoins
|
|
}
|
|
|
|
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
|
|
// 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
|
|
def.MomentumEnabled = cfg.MomentumEnabled || def.MomentumEnabled
|
|
def.TrendEnabled = cfg.TrendEnabled || def.TrendEnabled
|
|
|
|
return def
|
|
}
|