feat: 重构为三所价差异动监控系统

删除 HyperLiquid + 全部交易功能,构建自适应 surge 检测器。
- 新增 surge_detector.go: 每币独立滚动窗口基线,检测三所价差异常飙升
- 新增 SpreadCard/SurgeCard 前端组件
- 保留 momentum/trend/cumulative/trend_filter 扫描功能
- 更新文档和配置以反映新系统

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jackyu66git
2026-05-08 02:01:18 +08:00
co-authored by Claude Opus 4.6
parent 559d7bb870
commit d38782490c
36 changed files with 1201 additions and 6374 deletions
+42 -180
View File
@@ -4,7 +4,6 @@ import (
"encoding/json"
"os"
"strconv"
"time"
)
// Config holds all system configuration.
@@ -13,44 +12,15 @@ type Config struct {
TelegramBotToken string
TelegramChatID string
AlertCooldownSec int // seconds between alerts for same coin
ArbThreshold float64 // minimum net profit % to trigger alert
ArbThreshold float64 // minimum spread % 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
// 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
@@ -62,33 +32,20 @@ type Config struct {
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"`
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"`
@@ -100,18 +57,6 @@ type jsonConfig struct {
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 {
@@ -151,34 +96,12 @@ func LoadConfig() *Config {
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,
// 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),
@@ -190,14 +113,6 @@ func LoadConfig() *Config {
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", ""),
}
}
@@ -205,36 +120,22 @@ 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
// 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
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
}
@@ -255,58 +156,22 @@ func loadJSONConfig() jsonConfig {
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
// Surge detection JSON overrides
if cfg.SurgeWindowSize != 0 {
def.SurgeWindowSize = cfg.SurgeWindowSize
}
if cfg.TakerFeeHyperLiquid != 0 {
def.TakerFeeHyperLiquid = cfg.TakerFeeHyperLiquid
if cfg.SurgeBaselineMultiplier != 0 {
def.SurgeBaselineMultiplier = cfg.SurgeBaselineMultiplier
}
if cfg.TakeProfitPct != 0 {
def.TakeProfitPct = cfg.TakeProfitPct
if cfg.SurgeMinAbsSpreadPct != 0 {
def.SurgeMinAbsSpreadPct = cfg.SurgeMinAbsSpreadPct
}
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.SurgeCooldownSec != 0 {
def.SurgeCooldownSec = cfg.SurgeCooldownSec
}
if cfg.MomentumThresholdPct != 0 {
@@ -328,12 +193,9 @@ func loadJSONConfig() jsonConfig {
}
// 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
def.SurgeEnabled = cfg.SurgeEnabled || def.SurgeEnabled
return def
}