fix: isStockSymbol misclassifies crypto symbols (BTC/ETH/SOL) as stock tickers

Critical bug: isStockSymbol('BTC') returned true because 'BTC' is 3
uppercase letters and the suffix check only catches 'BTCUSDT'-style pairs.
This caused crypto trade commands to be routed to Alpaca (stock trader)
instead of crypto exchanges.

Fix: add knownCryptoSymbols map with 60+ common crypto base symbols.
Check this map first before the heuristic letter-count check.

Also includes:
- Unit tests for isStockSymbol (crypto vs stock classification)
- Alpaca support in handleSyncBalance/handleClosePosition API handlers
- Cancel orphaned SL/TP orders before manual position close
- Clear peak PnL cache on position close (prevent stale data)
- Lighter FormatQuantity uses actual market precision instead of hardcoded 4
This commit is contained in:
shinchan-zhai
2026-03-23 20:18:24 +08:00
parent 7b1cff23bc
commit 06824915a4
5 changed files with 127 additions and 4 deletions

View File

@@ -9,6 +9,7 @@ import (
"nofx/logger"
"nofx/store"
"nofx/trader"
"nofx/trader/alpaca"
"nofx/trader/aster"
"nofx/trader/binance"
"nofx/trader/bitget"
@@ -120,6 +121,12 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
} else {
createErr = fmt.Errorf("Lighter requires wallet address and API Key private key")
}
case "alpaca":
tempTrader = alpaca.NewAlpacaTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
exchangeCfg.Testnet,
)
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"})
return
@@ -284,6 +291,12 @@ func (s *Server) handleClosePosition(c *gin.Context) {
} else {
createErr = fmt.Errorf("Lighter requires wallet address and API Key private key")
}
case "alpaca":
tempTrader = alpaca.NewAlpacaTrader(
string(exchangeCfg.APIKey),
string(exchangeCfg.SecretKey),
exchangeCfg.Testnet,
)
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"})
return
@@ -318,6 +331,14 @@ func (s *Server) handleClosePosition(c *gin.Context) {
}
}
// Cancel existing stop-loss and take-profit orders BEFORE closing position
// Critical: orphaned SL/TP orders could trigger after position is closed and create unintended positions
if err := tempTrader.CancelStopOrders(req.Symbol); err != nil {
logger.Warnf(" ⚠️ Failed to cancel stop orders for %s: %v (proceeding with close)", req.Symbol, err)
} else {
logger.Infof(" 🗑️ Cancelled stop/TP orders for %s before manual close", req.Symbol)
}
// Execute close position operation
var result map[string]interface{}
var closeErr error