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

@@ -471,11 +471,37 @@ func (a *Agent) toolGetTradeHistory(argsJSON string) string {
return string(result)
}
// knownCryptoSymbols is a set of well-known cryptocurrency base symbols.
// Without this, isStockSymbol("BTC") would incorrectly return true because
// "BTC" is 3 uppercase letters and the suffix check only catches "BTCUSDT"-style pairs.
var knownCryptoSymbols = map[string]bool{
"BTC": true, "ETH": true, "SOL": true, "BNB": true, "XRP": true,
"DOGE": true, "ADA": true, "AVAX": true, "DOT": true, "LINK": true,
"PEPE": true, "SHIB": true, "ARB": true, "OP": true, "SUI": true,
"APT": true, "SEI": true, "TIA": true, "JUP": true, "WIF": true,
"NEAR": true, "ATOM": true, "FTM": true, "MATIC": true, "INJ": true,
"RENDER": true, "FET": true, "TAO": true, "WLD": true, "USDT": true,
"USDC": true, "BUSD": true, "DAI": true, "UNI": true, "AAVE": true,
"LDO": true, "MKR": true, "CRV": true, "PENDLE": true, "ENA": true,
"ONDO": true, "TRUMP": true, "TON": true, "TRX": true, "LTC": true,
"BCH": true, "ETC": true, "FIL": true, "ICP": true, "HBAR": true,
"VET": true, "ALGO": true, "SAND": true, "MANA": true, "AXS": true,
"GMT": true, "APE": true, "GALA": true, "IMX": true, "BLUR": true,
"STRK": true, "ZK": true, "W": true, "IO": true, "ZRO": true,
"BONK": true, "FLOKI": true, "ORDI": true, "STX": true, "RUNE": true,
}
// isStockSymbol heuristically determines if a symbol is a stock ticker (not crypto).
// Stock tickers are 1-5 uppercase letters without numeric suffixes like "USDT".
// Known crypto suffixes: USDT, BTC, ETH, BNB, USDC, BUSD.
// Known crypto base symbols (BTC, ETH, SOL etc.) are excluded.
func isStockSymbol(sym string) bool {
sym = strings.ToUpper(sym)
// Check known crypto base symbols first (critical: "BTC", "ETH" etc. are NOT stocks)
if knownCryptoSymbols[sym] {
return false
}
// If it already has a crypto quote suffix, it's crypto
cryptoSuffixes := []string{"USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"}
for _, suffix := range cryptoSuffixes {

65
agent/tools_test.go Normal file
View File

@@ -0,0 +1,65 @@
package agent
import "testing"
func TestIsStockSymbol(t *testing.T) {
tests := []struct {
sym string
want bool
}{
// Known crypto base symbols — must NOT be detected as stock
{"BTC", false},
{"ETH", false},
{"SOL", false},
{"BNB", false},
{"XRP", false},
{"DOGE", false},
{"ADA", false},
{"AVAX", false},
{"DOT", false},
{"LINK", false},
{"PEPE", false},
{"SHIB", false},
{"TRUMP", false},
{"USDT", false},
{"USDC", false},
{"W", false}, // single letter crypto
// Crypto pairs — must NOT be stock
{"BTCUSDT", false},
{"ETHUSDT", false},
{"SOLUSDT", false},
{"DOGEUSDT", false},
// Real stock tickers — must be detected as stock
{"AAPL", true},
{"TSLA", true},
{"NVDA", true},
{"MSFT", true},
{"GOOGL", true},
{"AMZN", true},
{"META", true},
{"AMD", true},
{"PLTR", true},
{"BA", true},
{"F", true}, // Ford — 1 letter
{"GM", true}, // 2 letters
{"JPM", true}, // 3 letters
// Mixed / edge cases
{"btc", false}, // lowercase crypto
{"aapl", true}, // lowercase stock (uppercased internally)
{"BTC123", false}, // not pure letters
{"123456", false}, // digits
{"", false},
}
for _, tt := range tests {
t.Run(tt.sym, func(t *testing.T) {
got := isStockSymbol(tt.sym)
if got != tt.want {
t.Errorf("isStockSymbol(%q) = %v, want %v", tt.sym, got, tt.want)
}
})
}
}