mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-10 22:36:58 +08:00
feat: Alpaca US stock trader integration (Tasks 8-11)
- trader/alpaca/: Full Trader interface implementation for Alpaca - Paper/Live trading support (market orders, stop/limit) - GetBalance, GetPositions, GetMarketPrice via Alpaca API - Fractional share support (4 decimal places) - Commission-free trading - Agent trade routing: stock symbols (AAPL, TSLA) → Alpaca - isStockSymbol() heuristic to distinguish crypto vs stock - execute_trade tool updated for stock buy/sell semantics - get_market_price works for both crypto and stocks - TraderManager + API: Alpaca registered as exchange type - Factory case in auto_trader.go - Config mapping in trader_manager.go - Temp trader creation in handler_trader.go for balance query - Frontend: PositionsPanel distinguishes stock vs crypto - US flag emoji for stock positions - Dollar prefix for stock PnL/prices - 'Shares' label instead of 'Qty' for stocks - System prompts updated for stock trading capabilities
This commit is contained in:
@@ -475,10 +475,10 @@ func (a *Agent) buildSystemPrompt(lang string) string {
|
||||
## 工具使用
|
||||
你可以调用以下工具来执行操作:
|
||||
- **search_stock** — 搜索股票(支持中文名、英文名、代码)。当用户提到你不认识的股票时,先用这个工具搜索。
|
||||
- **execute_trade** — 下单交易(做多/做空/平多/平空)。调用后会创建待确认订单,用户需回复"确认 trade_xxx"才会真正执行。
|
||||
- **get_positions** — 查看当前所有持仓
|
||||
- **execute_trade** — 下单交易(加密货币或美股)。美股:open_long=买入,close_long=卖出。调用后创建待确认订单,用户需回复"确认 trade_xxx"。
|
||||
- **get_positions** — 查看当前所有持仓(加密货币 + 股票)
|
||||
- **get_balance** — 查看账户余额
|
||||
- **get_market_price** — 获取交易所实时价格
|
||||
- **get_market_price** — 获取实时价格(加密货币或股票代码)
|
||||
|
||||
### 交易安全规则
|
||||
- 用户明确要求交易时才调用 execute_trade
|
||||
@@ -536,10 +536,10 @@ func (a *Agent) buildSystemPrompt(lang string) string {
|
||||
## Tools
|
||||
You can call these tools to take action:
|
||||
- **search_stock** — Search for stocks by name, ticker, or code. Covers A-share, HK, and US markets. Use when the user mentions an unknown stock.
|
||||
- **execute_trade** — Place a trade order (open_long/open_short/close_long/close_short). Creates a pending order that requires user confirmation.
|
||||
- **get_positions** — View all current open positions
|
||||
- **execute_trade** — Place a trade order (crypto or US stocks). For stocks: open_long=buy, close_long=sell. Creates a pending order that requires user confirmation.
|
||||
- **get_positions** — View all current open positions (crypto + stocks)
|
||||
- **get_balance** — View account balance and equity
|
||||
- **get_market_price** — Get real-time price from the exchange
|
||||
- **get_market_price** — Get real-time price from the exchange (crypto or stock symbol)
|
||||
|
||||
### Trade Safety Rules
|
||||
- Only call execute_trade when user explicitly requests a trade
|
||||
|
||||
@@ -40,7 +40,7 @@ func buildAgentTools() []mcp.Tool {
|
||||
Type: "function",
|
||||
Function: mcp.FunctionDef{
|
||||
Name: "execute_trade",
|
||||
Description: "Execute a trade order. Use this when the user explicitly asks to open/close a position. This will create a pending trade that requires user confirmation before execution.",
|
||||
Description: "Execute a trade order (crypto or US stocks). Use this when the user explicitly asks to open/close a position. For stocks (e.g. AAPL, TSLA), use open_long to buy and close_long to sell. This creates a pending trade that requires user confirmation.",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
@@ -51,7 +51,7 @@ func buildAgentTools() []mcp.Tool {
|
||||
},
|
||||
"symbol": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Trading symbol, e.g. BTCUSDT, ETHUSDT. Always append USDT if not present.",
|
||||
"description": "Trading symbol. For crypto: BTCUSDT, ETHUSDT. For US stocks: AAPL, TSLA, NVDA (no suffix needed).",
|
||||
},
|
||||
"quantity": map[string]any{
|
||||
"type": "number",
|
||||
@@ -86,13 +86,13 @@ func buildAgentTools() []mcp.Tool {
|
||||
Type: "function",
|
||||
Function: mcp.FunctionDef{
|
||||
Name: "get_market_price",
|
||||
Description: "Get the current market price for a symbol from the trader's exchange.",
|
||||
Description: "Get the current market price for a crypto or stock symbol.",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"symbol": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Trading symbol, e.g. BTCUSDT",
|
||||
"description": "Trading symbol, e.g. BTCUSDT for crypto, AAPL for stocks",
|
||||
},
|
||||
},
|
||||
"required": []string{"symbol"},
|
||||
@@ -205,7 +205,8 @@ func (a *Agent) toolExecuteTrade(_ context.Context, userID int64, lang, argsJSON
|
||||
|
||||
// Normalize symbol
|
||||
sym := strings.ToUpper(args.Symbol)
|
||||
if !strings.HasSuffix(sym, "USDT") {
|
||||
// Only append USDT for crypto symbols; stock tickers (e.g. AAPL, TSLA) stay as-is
|
||||
if !isStockSymbol(sym) && !strings.HasSuffix(sym, "USDT") {
|
||||
sym += "USDT"
|
||||
}
|
||||
|
||||
@@ -330,7 +331,7 @@ func (a *Agent) toolGetMarketPrice(argsJSON string) string {
|
||||
}
|
||||
|
||||
sym := strings.ToUpper(args.Symbol)
|
||||
if !strings.HasSuffix(sym, "USDT") {
|
||||
if !isStockSymbol(sym) && !strings.HasSuffix(sym, "USDT") {
|
||||
sym += "USDT"
|
||||
}
|
||||
|
||||
@@ -459,3 +460,31 @@ func (a *Agent) toolGetTradeHistory(argsJSON string) string {
|
||||
})
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func isStockSymbol(sym string) bool {
|
||||
sym = strings.ToUpper(sym)
|
||||
// If it already has a crypto quote suffix, it's crypto
|
||||
cryptoSuffixes := []string{"USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"}
|
||||
for _, suffix := range cryptoSuffixes {
|
||||
if strings.HasSuffix(sym, suffix) && len(sym) > len(suffix) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Pure uppercase letters, 1-5 chars = likely a stock ticker
|
||||
if len(sym) >= 1 && len(sym) <= 5 {
|
||||
allLetters := true
|
||||
for _, c := range sym {
|
||||
if c < 'A' || c > 'Z' {
|
||||
allLetters = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allLetters {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -153,6 +153,9 @@ func (a *Agent) executeTrade(ctx context.Context, trade *TradeAction) error {
|
||||
return fmt.Errorf("no traders configured")
|
||||
}
|
||||
|
||||
// Determine if this is a stock trade to route to the right exchange
|
||||
wantStock := isStockSymbol(trade.Symbol)
|
||||
|
||||
// Find a running trader's underlying exchange interface
|
||||
var underlyingTrader interface {
|
||||
OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error)
|
||||
@@ -169,12 +172,24 @@ func (a *Agent) executeTrade(ctx context.Context, trade *TradeAction) error {
|
||||
if ut == nil {
|
||||
continue
|
||||
}
|
||||
// Route stock symbols to alpaca traders, crypto to others
|
||||
exchange := t.GetExchange()
|
||||
isAlpaca := exchange == "alpaca"
|
||||
if wantStock && !isAlpaca {
|
||||
continue // Skip non-stock traders for stock symbols
|
||||
}
|
||||
if !wantStock && isAlpaca {
|
||||
continue // Skip stock traders for crypto symbols
|
||||
}
|
||||
underlyingTrader = ut
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if underlyingTrader == nil {
|
||||
if wantStock {
|
||||
return fmt.Errorf("no running stock trader (Alpaca) found — configure one to trade stocks")
|
||||
}
|
||||
return fmt.Errorf("no running trader supports trade execution")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user