From 915b316ca7fc55ef79c1cd76667eabd57e59197b Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Sun, 3 May 2026 22:25:01 +0800 Subject: [PATCH] fix: HL size floor, random scan jitter 50-250ms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GetHLSize: change rounding from Sprintf (round-to-nearest) to math.Floor (floor), consistent with GetBitgetSize - Scan interval: fixed 200ms → random 50-250ms to avoid lock-step with HyperLiquid's ~200ms allMids push cycle - README: update architecture diagram, trading logic, config note --- README.md | 9 ++++++--- exchange/hyperliquid_trade.go | 20 ++++++++++++++------ main.go | 13 +++++++++++-- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 794c1ce..095df86 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,8 @@ PriceStore ─────────┼───────────── └──────────────┘ │ ┌─────────▼─────────┐ - │ ScanBGHL (200ms) │ + │ ScanBGHL (50-250ms │ + │ random jitter) │ │ BG ↔ HL only │ └─────────┬─────────┘ │ @@ -92,11 +93,13 @@ Then open [http://localhost:8888](http://localhost:8888) for the Web dashboard. | `TRADE_THRESHOLD` | `0.15` | Min net profit % to enter (after fees) | | `TRADE_AMOUNT_USD` | `10` | USD per leg | | `TRADE_COOLDOWN_MS` | `30000` | Cooldown between same-coin trades (ms) | -| `TEST_MODE` | `false` | Simulate orders (no real API calls) | +| `TEST_MODE` | `false` | Simulate orders with mock fills (no real API calls) | | `MOCK_SLIPPAGE_PCT` | `0.005` | Simulated slippage per leg (%) | | `BITGET_API_KEY` / `BITGET_API_SECRET` / `BITGET_PASSPHRASE` | — | Bitget API credentials (test mode skips) | | `HL_PRIVATE_KEY` / `HL_ADDRESS` | — | HyperLiquid wallet credentials (test mode skips) | +> **Note:** Scan interval is fixed at **50-250ms random jitter** (not configurable). This prevents lock-step with HyperLiquid's ~200ms push cycle. + ## Fee Model All trades use **maker** (limit orders), no rebate. Only Bitget and HyperLiquid are used for trading: @@ -110,7 +113,7 @@ Round trip (2 legs entry + 2 legs exit): **0.07%** total fees. ## Trading Logic -1. **Scanner** runs every 500ms, checks all 6 coins for BG ↔ HL spread +1. **Scanner** runs every 50-250ms (random jitter to avoid lock-step with exchange push cycles), checks all 6 coins for BG ↔ HL spread 2. **Entry** when net profit ≥ `TRADE_THRESHOLD` (after full round-trip fees) - Uses scan-time prices directly (no re-read from store to avoid WS jitter) - Synchronous execution in the scanner tick (no goroutine delay) diff --git a/exchange/hyperliquid_trade.go b/exchange/hyperliquid_trade.go index 0573cde..171c1c2 100644 --- a/exchange/hyperliquid_trade.go +++ b/exchange/hyperliquid_trade.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "io" + "math" "math/big" "net/http" "strings" @@ -171,21 +172,28 @@ func (h *HyperLiquidTrade) signAction(action HLOrderAction, nonce int64) (string // GetHLSize calculates size for a given USD amount on HyperLiquid. // Uses szDecimals precision from HL's contract universe. // Returns size as a decimal string complying with HL precision. +// Uses math.Floor to round DOWN to the nearest valid step, consistent with GetBitgetSize. func GetHLSize(coin string, amountUSD, price float64) string { sz := amountUSD / price // raw coin count switch coin { case "DOGE": - return fmt.Sprintf("%.0f", sz) // szDecimals=0 + sz = math.Floor(sz) // step=1, szDecimals=0 + return fmt.Sprintf("%.0f", sz) case "LINK": - return fmt.Sprintf("%.1f", sz) // szDecimals=1 + sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1 + return fmt.Sprintf("%.1f", sz) case "ONDO": - return fmt.Sprintf("%.0f", sz) // szDecimals=0 + sz = math.Floor(sz) // step=1, szDecimals=0 + return fmt.Sprintf("%.0f", sz) case "OP": - return fmt.Sprintf("%.1f", sz) // szDecimals=1 + sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1 + return fmt.Sprintf("%.1f", sz) case "WIF": - return fmt.Sprintf("%.0f", sz) // szDecimals=0 + sz = math.Floor(sz) // step=1, szDecimals=0 + return fmt.Sprintf("%.0f", sz) case "ARB": - return fmt.Sprintf("%.1f", sz) // szDecimals=1 + sz = math.Floor(sz*10) / 10 // step=0.1, szDecimals=1 + return fmt.Sprintf("%.1f", sz) default: return fmt.Sprintf("%.4f", sz) } diff --git a/main.go b/main.go index c610a92..f9a1e3c 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log" + "math/rand" "os" "os/signal" "strings" @@ -113,10 +114,16 @@ func main() { // Main loop lastHour := -1 - scannerTick := time.NewTicker(time.Duration(cfg.ScanIntervalMs) * time.Millisecond) + + // Random jitter 50-250ms to avoid lock-step with exchange push cycles + jitterMin, jitterMax := 50, 250 + randInterval := func() time.Duration { + return time.Duration(jitterMin+rand.Intn(jitterMax-jitterMin+1)) * time.Millisecond + } + scannerTick := time.NewTimer(randInterval()) statusTick := time.NewTicker(30 * time.Second) - log.Printf("[Monitor] Scanner running every %dms", cfg.ScanIntervalMs) + log.Printf("[Monitor] Scanner running every %d-%dms (random jitter)", jitterMin, jitterMax) runLoop := true for runLoop { @@ -202,6 +209,8 @@ func main() { notifier.SendTradeSummary(positions, now.Format("2006-01-02 15:04")) lastHour = hour } + + scannerTick.Reset(randInterval()) } }