- ArbTable: removed slice(0,10) cap, shows all 42 opps (21x2)
- PriceTable: added net_bg_to_hl and net_hl_to_bg columns
- Backend SSE: sends net profit for both directions per coin
HL testnet USDC lives in the spot account, not the perp clearinghouse.
GetBalance() was calling UserState() (perp clearinghouseState), which
returned /usr/bin/bash. Switched to SpotUserState() and parse USDC.total - USDC.hold.
Also cleaned up .gitignore to exclude .env, binary, logs, data/.
- config.json: test_mode=false, ready for sim/testnet trading
- trader.go: auto-stop after 5 real trades, exchange response logging,
Stop()/Start() API, shuttingDown flag for graceful stop
- dashboard.go: POST /api/stop + POST /api/start endpoints,
trading status in SSE stats
- exchange/hyperliquid.go: switch HL WS to testnet endpoint
- exchange/hyperliquid_trade.go: switch REST to testnet endpoint,
support base64 + 32-byte EVM private keys
- main.go: listen on trader.StopCh (graceful, no process exit)
- scanner.go: trim TrackedCoins to only 6 core coins (DOGE/LINK/ONDO/OP/WIF/ARB)
- .gitignore: ignore main binary
1. GetHLSize: add minimum size checks (prevent size=0 when price > amount)
2. executeEntry: orphan position warning when leg1 close also fails after leg2
3. checkExit: keep position as close_failed on failure, retryClose on each Tick
4. persistTrade: sync instead of goroutine (prevent data loss on exit)
During async executeEntry goroutine, the 300ms sleep between legs
left the position vulnerable to checkExit() from the main loop.
Added intermediate Status='entering' that checkExit/checkScaleIn
skip ('entering' != 'open'), switched to 'open' only after both
legs are placed.
- TryEntry now spawns a goroutine for order placement
- Main loop continues at 50-250ms even during entry
- 'entering' map prevents duplicate entries on same coin
- Async cleanup of entering state on completion
- Remove DetailedStats struct and GetDetailedStats() method from trader.go
- Add calcDetailedStats() standalone pure function in dashboard.go
- Dashboard calls d.trader.GetClosedTrades() + calcDetailedStats()
- Trading logic now has zero display-oriented calculations
- Remove goroutine in TryEntry (executeEntry now synchronous)
- Use opp.BuyPrice/SellPrice directly instead of re-reading from store
- Keep lightweight direction sanity check (0.1% tolerance)
- executeEntry returns bool for call chain consistency
HL→BG position: long HL, short BG.
When bgP and hlP cross over (spread reverses):
- Long HL: price went up → profit
- Short BG: price went down → profit
Both legs profit simultaneously. Reversal is MAX profit moment.
- Add explicit stop-loss when diffPct < -0.02 (价差反转,止损平仓)
instead of relying on the convergence threshold to catch reversals
- Dashboard currentSpread now direction-aware for HL→BG positions
- Trade detail modal with clickable rows (previous commit partial)
Before: reversals would exit via with wrong reason
'价差收敛'. After: dedicated < -0.02 check with correct reason.
- Click any trade row in history table to open detail modal
- Modal shows 6 sections: 概览, 时间, 价差, 手续费, 多仓, 空仓
- Entries and exits displayed with 6 decimal precision
- Fee entry/exit and total fee displayed
- Open/close timestamps with full date-time format
- Duration, scale count, total amount, exit reason
- Orders sub-table if available
- Escape key and overlay click to close
- Add snapMu RWMutex + positionsSnapshot to Trader
- RefreshSnapshot() called from main loop after Tick() — acquires
t.mu briefly, stores deep copy under snapMu
- ReadSnapshot() returns snapshot copy under snapMu.RLock — never
touches t.mu, zero contention with trading path
- Dashboard + handleStatus + hourly summary + status log all
use ReadSnapshot() instead of GetPositionsCopy()
- Trading path (Tick/TryEntry/executeEntry/checkExit/checkScaleIn)
never blocked by display reads
- Snapshot is at most 1 tick behind live state — acceptable delay
- 🔴 Data race: Add GetPositionsCopy() returning deep copies (no shared
ArbPosition pointers). Use it in dashboard broadcastLoop + handleStatus.
- 🟡 Scale-in PnL: Track LongEntryPrices/ShortEntryPrices on ArbPosition,
compute weighted average (harmonic mean) at exit for accurate PnL.
- 🟢 CalcNetProfit: Delete dead code from exchange/helpers.go.
- 🟢 HL nonce: Add sync.Mutex around lastNonce++ (thread safety).
- 🟢 Hourly check: Change from 5-second window to minute window.
- 🟢 ExitPrice: Test mode closeLeg already handled by checkExit.
Issue #1 (critical): reProfit false positive on price reversal
executeEntry used CalcNetProfit which auto-swaps prices when
hlP < bgP. If prices flipped between scan and execution, reProfit
reported positive even when direction was now wrong.
Fix: use netProfit() with explicit direction + verify spread
direction hasn't flipped (hlP <= bgP prevents BG->HL when
HL is no longer more expensive).
Issue #2 (medium): Scale-in was paper-only, didn't place orders
Now actually places additional orders on both legs via new
placeOrderAt(). Test mode uses mock fills. Live mode sends
real API orders. AmountUSD properly tracks total deployed
capital. Partial fill handled gracefully (don't close main leg).
Issue #3 (minor): closeLeg missing ExitTime on mock mode
P3-1: Scan optimization — only BG↔HL (50+ pair combos → 2)
P3-2: Real-time spread chart — spreadHistory ring buffer +
/api/spread-history endpoint + Chart.js spread chart
P3-3: Live position PnL — positions SSE now includes
estimated current profit/loss + current spread
P3-4: Real-time trade events — trader.OnTradeEvent callback
fires SSE 'trade_open' / 'trade_close' immediately
P3-5: Connection status monitoring — tracks last update time
per exchange, broadcast via stats.connections + /api/connections
Frontend: spread chart card, PnL column in positions,
connection status dots in stats bar,
green/red border flash on trade events
B#1 — sigCh shared across goroutines, SIGINT unreliable
→ context.WithCancel: main loop cancels ctx on SIGINT,
4 WS goroutines select on ctx.Done() instead of shared sigCh
B#3 — restoreOpenPositions missing LastScaleAt
→ Set LastScaleAt = tr.OpenedAt on restore so scale-in cooldown works
B#4 — dYdX heartbeat goroutine leaks on reconnect
→ Added stopHeartbeat chan + heartbeatMu mutex; close old channel
before spawning new heartbeat goroutine
B#5 — GetBitgetSize fmt.Sprintf rounds up, may exceed amountUSD
→ Added math.Floor(sz*multiplier)/multiplier before format to round
DOWN to nearest valid step size for every coin
B#6 — netProfit and CalcNetProfit duplicate formula
→ scanner.go netProfit now delegates to exchange.CalcNetProfit
B#7 — Aevo Run callback only 2 params, incompatible with startExchange
→ Changed to 4-arg callback func(coin, price, bid, ask) with bid=ask=0
B#8 — parseFloat uses fmt.Sscanf (slow, locale-sensitive)
→ Replaced with strconv.ParseFloat
B#9 — dYdX receives hlSymbols instead of its own symbol list
→ Added dydxSymbols var, built from c.HL like other exchanges