From 4fa375771d531aa56207f0b7fe1035e1ebba745d Mon Sep 17 00:00:00 2001 From: shinchan-zhai Date: Mon, 23 Mar 2026 02:27:25 +0800 Subject: [PATCH] feat(agent): LLM tool-calling for auto trade execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New agent/tools.go: defines LLM tools (execute_trade, get_positions, get_balance, get_market_price) - thinkAndAct now uses CallWithRequestFull with tool-calling loop (max 5 rounds) — LLM autonomously decides when to call tools - Trade safety: execute_trade creates pending orders requiring user confirmation ('确认 trade_xxx' / 'confirm trade_xxx') - Pending trades expire after 5 minutes - System prompt updated with tool usage instructions and safety rules - Both paths work: regex shortcut ('做多 BTC 0.01') and natural language ('帮我开一个BTC多单') via LLM tool calling - Fallback: if tool-calling fails, degrades to plain LLM call --- agent/agent.go | 126 +++++++++++++++++-- agent/tools.go | 260 ++++++++++++++++++++++++++++++++++++++++ agent/trade.go | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 695 insertions(+), 11 deletions(-) create mode 100644 agent/tools.go create mode 100644 agent/trade.go diff --git a/agent/agent.go b/agent/agent.go index d71cb728..9a5dbc3f 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -30,6 +30,7 @@ type Agent struct { scheduler *Scheduler logger *slog.Logger history *chatHistory + pending *pendingTrades NotifyFunc func(userID int64, text string) error } @@ -51,7 +52,7 @@ func DefaultConfig() *Config { func New(tm *manager.TraderManager, st *store.Store, cfg *Config, logger *slog.Logger) *Agent { if cfg == nil { cfg = DefaultConfig() } - return &Agent{traderManager: tm, store: st, config: cfg, logger: logger, history: newChatHistory(20)} + return &Agent{traderManager: tm, store: st, config: cfg, logger: logger, history: newChatHistory(20), pending: newPendingTrades()} } func (a *Agent) SetAIClient(c mcp.AIClient) { a.aiClient = c } @@ -133,12 +134,25 @@ func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (s return "🧹 Conversation history cleared.", nil } + // Check for trade confirmation (e.g. "确认 trade_xxx" or "confirm trade_xxx") + if resp, handled := a.handleTradeConfirmation(ctx, userID, text, lang); handled { + return resp, nil + } + + // Check for direct trade commands (e.g. "做多 BTC 0.01") + if trade := parseTradeCommand(text); trade != nil { + a.pending.Add(trade) + a.pending.CleanExpired() + return formatTradeConfirmation(trade, lang), nil + } + // EVERYTHING else → LLM with tools return a.thinkAndAct(ctx, userID, lang, text) } // thinkAndAct sends the user message to LLM with full context and tools. // The LLM decides what to do — analyze, query, trade, or just chat. +// Supports a tool-calling loop: LLM can call tools, get results, and continue. func (a *Agent) thinkAndAct(ctx context.Context, userID int64, lang, text string) (string, error) { if a.aiClient == nil { return a.noAIFallback(lang, text) @@ -170,18 +184,82 @@ func (a *Agent) thinkAndAct(ctx context.Context, userID int64, lang, text string // Record user message in history a.history.Add(userID, "user", text) - // Call LLM with full conversation context - req := &mcp.Request{Messages: messages} - resp, err := a.aiClient.CallWithRequest(req) - if err != nil { - a.logger.Error("LLM call failed", "error", err) - return a.noAIFallback(lang, text) + // Define tools for the LLM + tools := agentTools() + + // Tool-calling loop (max 5 iterations to prevent infinite loops) + const maxToolRounds = 5 + for round := 0; round < maxToolRounds; round++ { + req := &mcp.Request{ + Messages: messages, + Tools: tools, + ToolChoice: "auto", + } + + resp, err := a.aiClient.CallWithRequestFull(req) + if err != nil { + a.logger.Error("LLM call failed", "error", err, "round", round) + if round == 0 { + // First round failed — try without tools as fallback + plainReq := &mcp.Request{Messages: messages} + plainResp, plainErr := a.aiClient.CallWithRequest(plainReq) + if plainErr != nil { + return a.noAIFallback(lang, text) + } + a.history.Add(userID, "assistant", plainResp) + return plainResp, nil + } + return a.noAIFallback(lang, text) + } + + // If LLM returned a text response (no tool calls), we're done + if len(resp.ToolCalls) == 0 { + a.history.Add(userID, "assistant", resp.Content) + return resp.Content, nil + } + + // LLM wants to call tools — process each one + a.logger.Info("LLM tool calls", "count", len(resp.ToolCalls), "round", round) + + // Add assistant message with tool calls to conversation + assistantMsg := mcp.Message{ + Role: "assistant", + ToolCalls: resp.ToolCalls, + } + if resp.Content != "" { + assistantMsg.Content = resp.Content + } + messages = append(messages, assistantMsg) + + // Execute each tool call and add results + for _, tc := range resp.ToolCalls { + a.logger.Info("executing tool", + "name", tc.Function.Name, + "args", tc.Function.Arguments, + "call_id", tc.ID, + ) + + result := a.handleToolCall(ctx, userID, lang, tc) + + // Add tool result message + messages = append(messages, mcp.Message{ + Role: "tool", + Content: result, + ToolCallID: tc.ID, + }) + } + + // Continue the loop — LLM will see tool results and either respond or call more tools } - // Record assistant response in history - a.history.Add(userID, "assistant", resp) - - return resp, nil + // If we exhausted all rounds, ask LLM for a final text response without tools + finalReq := &mcp.Request{Messages: messages} + finalResp, err := a.aiClient.CallWithRequest(finalReq) + if err != nil { + return a.noAIFallback(lang, text) + } + a.history.Add(userID, "assistant", finalResp) + return finalResp, nil } // buildSystemPrompt creates the system prompt that makes NOFXi behave like a real agent. @@ -214,6 +292,19 @@ func (a *Agent) buildSystemPrompt(lang string) string { - 必须明确告诉用户"以下分析基于历史知识,不含实时数据,请以实际行情为准" - 可以分析趋势、逻辑、策略框架,但具体价位必须让用户自己查看 +## 工具使用 +你可以调用以下工具来执行操作: +- **execute_trade** — 下单交易(做多/做空/平多/平空)。调用后会创建待确认订单,用户需回复"确认 trade_xxx"才会真正执行。 +- **get_positions** — 查看当前所有持仓 +- **get_balance** — 查看账户余额 +- **get_market_price** — 获取交易所实时价格 + +### 交易安全规则 +- 用户明确要求交易时才调用 execute_trade +- 分析和建议不需要调用工具,直接回复即可 +- 交易确认信息要清晰展示:品种、方向、数量、杠杆 +- 提醒用户确认命令格式 + ## 行为准则 - 简洁、专业、有观点。不说废话。 - 用户问什么答什么,不要推销配置。 @@ -245,6 +336,19 @@ func (a *Agent) buildSystemPrompt(lang string) string { - Must tell user: "Analysis based on historical knowledge, not live data. Verify with actual quotes." - Can analyze trends, logic, strategy frameworks — but specific prices must be verified by user +## Tools +You can call these tools to take action: +- **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 +- **get_balance** — View account balance and equity +- **get_market_price** — Get real-time price from the exchange + +### Trade Safety Rules +- Only call execute_trade when user explicitly requests a trade +- Analysis and advice don't need tools — just reply directly +- Show trade details clearly: symbol, direction, quantity, leverage +- Remind user of the confirmation command format + ## Behavior - Concise, professional, opinionated. No fluff. - Answer what's asked. Don't push setup. diff --git a/agent/tools.go b/agent/tools.go new file mode 100644 index 00000000..c12996fd --- /dev/null +++ b/agent/tools.go @@ -0,0 +1,260 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "nofx/mcp" +) + +// agentTools defines the tools available to the LLM for autonomous action. +func agentTools() []mcp.Tool { + return []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.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "action": map[string]any{ + "type": "string", + "enum": []string{"open_long", "open_short", "close_long", "close_short"}, + "description": "Trade action: open_long (做多/buy), open_short (做空/sell), close_long (平多), close_short (平空)", + }, + "symbol": map[string]any{ + "type": "string", + "description": "Trading symbol, e.g. BTCUSDT, ETHUSDT. Always append USDT if not present.", + }, + "quantity": map[string]any{ + "type": "number", + "description": "Trade quantity/amount. Required for opening positions. Use 0 to close entire position.", + }, + "leverage": map[string]any{ + "type": "number", + "description": "Leverage multiplier (e.g. 5, 10, 20). Optional, defaults to trader's current setting.", + }, + }, + "required": []string{"action", "symbol", "quantity"}, + }, + }, + }, + { + Type: "function", + Function: mcp.FunctionDef{ + Name: "get_positions", + Description: "Get all current open positions across all traders. Returns symbol, side, size, entry price, mark price, and unrealized PnL.", + Parameters: map[string]any{"type": "object", "properties": map[string]any{}}, + }, + }, + { + Type: "function", + Function: mcp.FunctionDef{ + Name: "get_balance", + Description: "Get account balance and equity across all traders.", + Parameters: map[string]any{"type": "object", "properties": map[string]any{}}, + }, + }, + { + Type: "function", + Function: mcp.FunctionDef{ + Name: "get_market_price", + Description: "Get the current market price for a symbol from the trader's exchange.", + Parameters: map[string]any{ + "type": "object", + "properties": map[string]any{ + "symbol": map[string]any{ + "type": "string", + "description": "Trading symbol, e.g. BTCUSDT", + }, + }, + "required": []string{"symbol"}, + }, + }, + }, + } +} + +// handleToolCall processes a single tool call from the LLM and returns the result. +func (a *Agent) handleToolCall(ctx context.Context, userID int64, lang string, tc mcp.ToolCall) string { + switch tc.Function.Name { + case "execute_trade": + return a.toolExecuteTrade(ctx, userID, lang, tc.Function.Arguments) + case "get_positions": + return a.toolGetPositions() + case "get_balance": + return a.toolGetBalance() + case "get_market_price": + return a.toolGetMarketPrice(tc.Function.Arguments) + default: + return fmt.Sprintf(`{"error": "unknown tool: %s"}`, tc.Function.Name) + } +} + +func (a *Agent) toolExecuteTrade(_ context.Context, userID int64, lang, argsJSON string) string { + var args struct { + Action string `json:"action"` + Symbol string `json:"symbol"` + Quantity float64 `json:"quantity"` + Leverage int `json:"leverage"` + } + if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { + return fmt.Sprintf(`{"error": "invalid arguments: %s"}`, err) + } + + // Normalize symbol + sym := strings.ToUpper(args.Symbol) + if !strings.HasSuffix(sym, "USDT") { + sym += "USDT" + } + + // Validate action + validActions := map[string]bool{ + "open_long": true, "open_short": true, + "close_long": true, "close_short": true, + } + if !validActions[args.Action] { + return fmt.Sprintf(`{"error": "invalid action: %s"}`, args.Action) + } + + // For open actions, quantity must be > 0 + if (args.Action == "open_long" || args.Action == "open_short") && args.Quantity <= 0 { + return `{"error": "quantity must be > 0 for opening positions"}` + } + + // Create pending trade — requires user confirmation + trade := &TradeAction{ + ID: fmt.Sprintf("trade_%d", time.Now().UnixNano()), + Action: args.Action, + Symbol: sym, + Quantity: args.Quantity, + Leverage: args.Leverage, + Status: "pending_confirmation", + CreatedAt: time.Now().Unix(), + } + + a.pending.Add(trade) + a.pending.CleanExpired() + + // Return confirmation info to LLM so it can present it to the user + result, _ := json.Marshal(map[string]any{ + "status": "pending_confirmation", + "trade_id": trade.ID, + "action": trade.Action, + "symbol": trade.Symbol, + "quantity": trade.Quantity, + "leverage": trade.Leverage, + "message": fmt.Sprintf("Trade created. User must confirm with: 确认 %s (or: confirm %s)", trade.ID, trade.ID), + "expires": "5 minutes", + }) + return string(result) +} + +func (a *Agent) toolGetPositions() string { + if a.traderManager == nil { + return `{"error": "no trader manager configured"}` + } + + var positions []map[string]any + for id, t := range a.traderManager.GetAllTraders() { + pos, err := t.GetPositions() + if err != nil { + continue + } + for _, p := range pos { + size := toFloat(p["size"]) + if size == 0 { + continue + } + tid := id + if len(tid) > 8 { + tid = tid[:8] + } + positions = append(positions, map[string]any{ + "trader": tid, + "symbol": p["symbol"], + "side": p["side"], + "size": size, + "entry_price": toFloat(p["entryPrice"]), + "mark_price": toFloat(p["markPrice"]), + "unrealized_pnl": toFloat(p["unrealizedPnl"]), + "leverage": p["leverage"], + }) + } + } + + if len(positions) == 0 { + return `{"positions": [], "message": "no open positions"}` + } + + result, _ := json.Marshal(map[string]any{"positions": positions}) + return string(result) +} + +func (a *Agent) toolGetBalance() string { + if a.traderManager == nil { + return `{"error": "no trader manager configured"}` + } + + var balances []map[string]any + for id, t := range a.traderManager.GetAllTraders() { + info, err := t.GetAccountInfo() + if err != nil { + continue + } + tid := id + if len(tid) > 8 { + tid = tid[:8] + } + balances = append(balances, map[string]any{ + "trader": tid, + "name": t.GetName(), + "exchange": t.GetExchange(), + "total_equity": toFloat(info["total_equity"]), + "available": toFloat(info["available_balance"]), + "used_margin": toFloat(info["used_margin"]), + }) + } + + result, _ := json.Marshal(map[string]any{"balances": balances}) + return string(result) +} + +func (a *Agent) toolGetMarketPrice(argsJSON string) string { + var args struct { + Symbol string `json:"symbol"` + } + if err := json.Unmarshal([]byte(argsJSON), &args); err != nil { + return fmt.Sprintf(`{"error": "invalid arguments: %s"}`, err) + } + + sym := strings.ToUpper(args.Symbol) + if !strings.HasSuffix(sym, "USDT") { + sym += "USDT" + } + + if a.traderManager == nil { + return `{"error": "no trader manager configured"}` + } + + for _, t := range a.traderManager.GetAllTraders() { + underlying := t.GetUnderlyingTrader() + if underlying == nil { + continue + } + price, err := underlying.GetMarketPrice(sym) + if err == nil && price > 0 { + result, _ := json.Marshal(map[string]any{ + "symbol": sym, + "price": price, + }) + return string(result) + } + } + + return fmt.Sprintf(`{"error": "could not get price for %s"}`, sym) +} diff --git a/agent/trade.go b/agent/trade.go new file mode 100644 index 00000000..85f1d9ec --- /dev/null +++ b/agent/trade.go @@ -0,0 +1,320 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + "sync" + "time" +) + +// TradeAction represents a parsed trade intent from the LLM or user. +type TradeAction struct { + ID string `json:"id"` + Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short" + Symbol string `json:"symbol"` // e.g. "BTCUSDT" + Quantity float64 `json:"quantity"` // amount + Leverage int `json:"leverage"` // leverage multiplier + TraderID string `json:"trader_id"` // which trader to use + Status string `json:"status"` // "pending", "confirmed", "executed", "failed", "expired" + CreatedAt int64 `json:"created_at"` + Error string `json:"error,omitempty"` +} + +// pendingTrades stores pending trade confirmations. +type pendingTrades struct { + mu sync.RWMutex + trades map[string]*TradeAction // id -> trade +} + +func newPendingTrades() *pendingTrades { + return &pendingTrades{trades: make(map[string]*TradeAction)} +} + +func (p *pendingTrades) Add(t *TradeAction) { + p.mu.Lock() + defer p.mu.Unlock() + p.trades[t.ID] = t +} + +func (p *pendingTrades) Get(id string) *TradeAction { + p.mu.RLock() + defer p.mu.RUnlock() + return p.trades[id] +} + +func (p *pendingTrades) Remove(id string) { + p.mu.Lock() + defer p.mu.Unlock() + delete(p.trades, id) +} + +// CleanExpired removes trades older than 5 minutes. +func (p *pendingTrades) CleanExpired() { + p.mu.Lock() + defer p.mu.Unlock() + cutoff := time.Now().Add(-5 * time.Minute).Unix() + for id, t := range p.trades { + if t.CreatedAt < cutoff { + delete(p.trades, id) + } + } +} + +// parseTradeCommand parses natural language trade commands. +// Returns nil if the message is not a trade command. +func parseTradeCommand(text string) *TradeAction { + upper := strings.ToUpper(strings.TrimSpace(text)) + + // Pattern: "做多 BTC 0.01" / "做空 ETH 0.1" / "long BTC 0.01" / "short ETH 0.1" + // Also: "平多 BTC" / "平空 ETH" / "close long BTC" / "close short ETH" + + var action, symbol string + var quantity float64 + var leverage int + + words := strings.Fields(upper) + if len(words) < 2 { + return nil + } + + switch words[0] { + case "做多", "LONG", "BUY": + action = "open_long" + case "做空", "SHORT", "SELL": + action = "open_short" + case "平多": + action = "close_long" + case "平空": + action = "close_short" + case "CLOSE": + if len(words) >= 3 { + switch words[1] { + case "LONG": + action = "close_long" + words = append(words[:1], words[2:]...) // remove "LONG" + case "SHORT": + action = "close_short" + words = append(words[:1], words[2:]...) // remove "SHORT" + } + } + if action == "" { + return nil + } + default: + return nil + } + + // Parse symbol + if len(words) < 2 { + return nil + } + symbol = words[1] + if !strings.HasSuffix(symbol, "USDT") { + symbol += "USDT" + } + + // Parse quantity (optional) + if len(words) >= 3 { + fmt.Sscanf(words[2], "%f", &quantity) + } + + // Parse leverage (optional, "x10" or "10x") + if len(words) >= 4 { + lev := strings.TrimSuffix(strings.TrimPrefix(words[3], "X"), "X") + fmt.Sscanf(lev, "%d", &leverage) + } + + if action == "" || symbol == "" { + return nil + } + + return &TradeAction{ + ID: fmt.Sprintf("trade_%d", time.Now().UnixNano()), + Action: action, + Symbol: symbol, + Quantity: quantity, + Leverage: leverage, + Status: "pending", + CreatedAt: time.Now().Unix(), + } +} + +// executeTrade performs the actual trade execution via TraderManager. +func (a *Agent) executeTrade(ctx context.Context, trade *TradeAction) error { + if a.traderManager == nil { + return fmt.Errorf("no trader manager available") + } + + traders := a.traderManager.GetAllTraders() + if len(traders) == 0 { + return fmt.Errorf("no traders configured") + } + + // Find a running trader's underlying exchange interface + var underlyingTrader interface { + OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) + OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) + CloseLong(symbol string, quantity float64) (map[string]interface{}, error) + CloseShort(symbol string, quantity float64) (map[string]interface{}, error) + } + + for _, t := range traders { + s := t.GetStatus() + running, _ := s["is_running"].(bool) + if running { + ut := t.GetUnderlyingTrader() + if ut == nil { + continue + } + underlyingTrader = ut + break + } + } + + if underlyingTrader == nil { + return fmt.Errorf("no running trader supports trade execution") + } + + switch trade.Action { + case "open_long": + if trade.Quantity <= 0 { + return fmt.Errorf("quantity must be > 0") + } + _, err := underlyingTrader.OpenLong(trade.Symbol, trade.Quantity, trade.Leverage) + return err + case "open_short": + if trade.Quantity <= 0 { + return fmt.Errorf("quantity must be > 0") + } + _, err := underlyingTrader.OpenShort(trade.Symbol, trade.Quantity, trade.Leverage) + return err + case "close_long": + _, err := underlyingTrader.CloseLong(trade.Symbol, trade.Quantity) + return err + case "close_short": + _, err := underlyingTrader.CloseShort(trade.Symbol, trade.Quantity) + return err + default: + return fmt.Errorf("unknown action: %s", trade.Action) + } +} + +// formatTradeConfirmation creates a confirmation message for a pending trade. +func formatTradeConfirmation(trade *TradeAction, lang string) string { + actionNames := map[string]string{ + "open_long": "做多 (Long)", + "open_short": "做空 (Short)", + "close_long": "平多 (Close Long)", + "close_short": "平空 (Close Short)", + } + + symbol := strings.TrimSuffix(trade.Symbol, "USDT") + actionName := actionNames[trade.Action] + if actionName == "" { + actionName = trade.Action + } + + if lang == "zh" { + msg := fmt.Sprintf("⚠️ **交易确认**\n\n"+ + "操作: %s\n"+ + "品种: %s\n", actionName, symbol) + if trade.Quantity > 0 { + msg += fmt.Sprintf("数量: %.4f\n", trade.Quantity) + } + if trade.Leverage > 0 { + msg += fmt.Sprintf("杠杆: %dx\n", trade.Leverage) + } + msg += fmt.Sprintf("\n发送 `确认 %s` 执行交易,或忽略取消。", trade.ID) + return msg + } + + msg := fmt.Sprintf("⚠️ **Trade Confirmation**\n\n"+ + "Action: %s\n"+ + "Symbol: %s\n", actionName, symbol) + if trade.Quantity > 0 { + msg += fmt.Sprintf("Quantity: %.4f\n", trade.Quantity) + } + if trade.Leverage > 0 { + msg += fmt.Sprintf("Leverage: %dx\n", trade.Leverage) + } + msg += fmt.Sprintf("\nSend `confirm %s` to execute, or ignore to cancel.", trade.ID) + return msg +} + +// handleTradeConfirmation processes a trade confirmation message. +func (a *Agent) handleTradeConfirmation(ctx context.Context, userID int64, text, lang string) (string, bool) { + upper := strings.ToUpper(strings.TrimSpace(text)) + + var tradeID string + if strings.HasPrefix(upper, "确认 ") || strings.HasPrefix(upper, "CONFIRM ") { + parts := strings.Fields(text) + if len(parts) >= 2 { + tradeID = parts[1] + } + } + + if tradeID == "" { + return "", false + } + + if a.pending == nil { + return "", false + } + + trade := a.pending.Get(tradeID) + if trade == nil { + if lang == "zh" { + return "❌ 交易已过期或不存在。", true + } + return "❌ Trade expired or not found.", true + } + + a.pending.Remove(tradeID) + trade.Status = "confirmed" + + a.logger.Info("executing trade", + slog.String("id", trade.ID), + slog.String("action", trade.Action), + slog.String("symbol", trade.Symbol), + slog.Float64("quantity", trade.Quantity), + ) + + err := a.executeTrade(ctx, trade) + if err != nil { + trade.Status = "failed" + trade.Error = err.Error() + if lang == "zh" { + return fmt.Sprintf("❌ 交易执行失败: %s", err.Error()), true + } + return fmt.Sprintf("❌ Trade execution failed: %s", err.Error()), true + } + + trade.Status = "executed" + symbol := strings.TrimSuffix(trade.Symbol, "USDT") + actionEmoji := "📈" + if strings.Contains(trade.Action, "short") { + actionEmoji = "📉" + } + if strings.Contains(trade.Action, "close") { + actionEmoji = "✅" + } + + qtyStr := "" + if trade.Quantity > 0 { + qtyStr = fmt.Sprintf(" %.4f", trade.Quantity) + } + + if lang == "zh" { + return fmt.Sprintf("%s 交易已执行!\n%s %s%s", actionEmoji, trade.Action, symbol, qtyStr), true + } + return fmt.Sprintf("%s Trade executed!\n%s %s%s", actionEmoji, trade.Action, symbol, qtyStr), true +} + +// marshals trade action to JSON for embedding in responses +func marshalTradeAction(trade *TradeAction) string { + b, _ := json.Marshal(trade) + return string(b) +}