mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 16:26:57 +08:00
fix: prevent Agent from fabricating position/balance data
- Add strict data truthfulness rules to system prompt (CN + EN) - Positions MUST come from get_positions tool only - Balance MUST come from get_balance tool only - Looking up a stock price != user owns it - Fix unused variable warnings in hyperliquid trader
This commit is contained in:
@@ -308,11 +308,19 @@ func (a *Agent) buildSystemPrompt(lang string) string {
|
|||||||
- 交易确认信息要清晰展示:品种、方向、数量、杠杆
|
- 交易确认信息要清晰展示:品种、方向、数量、杠杆
|
||||||
- 提醒用户确认命令格式
|
- 提醒用户确认命令格式
|
||||||
|
|
||||||
|
### 数据真实性规则(极其重要!)
|
||||||
|
- **持仓信息必须且只能通过 get_positions 工具获取**,绝对禁止编造持仓
|
||||||
|
- **余额信息必须且只能通过 get_balance 工具获取**,绝对禁止编造余额
|
||||||
|
- 如果用户问持仓但 get_positions 返回空,就说"当前没有持仓",不要编造
|
||||||
|
- 如果工具返回 error(如未配置交易所),如实告知用户
|
||||||
|
- **你不知道用户持有什么股票/币种,除非工具返回了数据**
|
||||||
|
- 查股票行情 ≠ 用户持有该股票。不要混淆"查价格"和"有持仓"
|
||||||
|
|
||||||
## 行为准则
|
## 行为准则
|
||||||
- 简洁、专业、有观点。不说废话。
|
- 简洁、专业、有观点。不说废话。
|
||||||
- 用户问什么答什么,不要推销配置。
|
- 用户问什么答什么,不要推销配置。
|
||||||
- 有实时数据时给具体价位,没有时给策略框架和思路。
|
- 有实时数据时给具体价位,没有时给策略框架和思路。
|
||||||
- **诚实是第一原则** — 不确定就说不确定,没数据就说没数据。
|
- **诚实是第一原则** — 不确定就说不确定,没数据就说没数据。绝不编造。
|
||||||
- 用交易相关的 emoji 让回复更直观。
|
- 用交易相关的 emoji 让回复更直观。
|
||||||
- 用中文回复。
|
- 用中文回复。
|
||||||
|
|
||||||
@@ -355,6 +363,14 @@ You can call these tools to take action:
|
|||||||
- Show trade details clearly: symbol, direction, quantity, leverage
|
- Show trade details clearly: symbol, direction, quantity, leverage
|
||||||
- Remind user of the confirmation command format
|
- Remind user of the confirmation command format
|
||||||
|
|
||||||
|
### Data Truthfulness Rules (CRITICAL!)
|
||||||
|
- **Position data MUST come from get_positions tool only** — NEVER fabricate positions
|
||||||
|
- **Balance data MUST come from get_balance tool only** — NEVER fabricate balances
|
||||||
|
- If get_positions returns empty, say "no open positions" — do NOT make up holdings
|
||||||
|
- If a tool returns an error (e.g. no exchange configured), tell the user honestly
|
||||||
|
- **You do NOT know what the user holds unless a tool tells you**
|
||||||
|
- Checking a stock price ≠ user owns that stock. Never confuse "quote lookup" with "holding"
|
||||||
|
|
||||||
## Behavior
|
## Behavior
|
||||||
- Concise, professional, opinionated. No fluff.
|
- Concise, professional, opinionated. No fluff.
|
||||||
- Answer what's asked. Don't push setup.
|
- Answer what's asked. Don't push setup.
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ import (
|
|||||||
type Brain struct {
|
type Brain struct {
|
||||||
agent *Agent
|
agent *Agent
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
|
http *http.Client
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
recentSignals sync.Map // debounce
|
recentSignals sync.Map // debounce
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBrain(agent *Agent, logger *slog.Logger) *Brain {
|
func NewBrain(agent *Agent, logger *slog.Logger) *Brain {
|
||||||
return &Brain{agent: agent, logger: logger, stopCh: make(chan struct{})}
|
return &Brain{
|
||||||
|
agent: agent,
|
||||||
|
logger: logger,
|
||||||
|
http: &http.Client{Timeout: 15 * time.Second},
|
||||||
|
stopCh: make(chan struct{}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Brain) Stop() { close(b.stopCh) }
|
func (b *Brain) Stop() { close(b.stopCh) }
|
||||||
@@ -57,10 +63,11 @@ func (b *Brain) StartNewsScan(interval time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *Brain) scanNews(seen map[string]bool) {
|
func (b *Brain) scanNews(seen map[string]bool) {
|
||||||
resp, err := http.Get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest")
|
resp, err := b.http.Get("https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest")
|
||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil { return }
|
||||||
|
|
||||||
var result struct {
|
var result struct {
|
||||||
Data []struct {
|
Data []struct {
|
||||||
@@ -72,7 +79,7 @@ func (b *Brain) scanNews(seen map[string]bool) {
|
|||||||
PublishedOn int64 `json:"published_on"`
|
PublishedOn int64 `json:"published_on"`
|
||||||
} `json:"Data"`
|
} `json:"Data"`
|
||||||
}
|
}
|
||||||
json.Unmarshal(body, &result)
|
if err := json.Unmarshal(body, &result); err != nil { return }
|
||||||
|
|
||||||
bullish := []string{"surge", "rally", "bullish", "breakout", "ath", "pump", "adoption"}
|
bullish := []string{"surge", "rally", "bullish", "breakout", "ath", "pump", "adoption"}
|
||||||
bearish := []string{"crash", "dump", "bearish", "sell-off", "plunge", "hack", "ban", "fraud"}
|
bearish := []string{"crash", "dump", "bearish", "sell-off", "plunge", "hack", "ban", "fraud"}
|
||||||
@@ -129,12 +136,13 @@ func (b *Brain) sendBrief(hour int) {
|
|||||||
// Fetch BTC/ETH prices for the brief
|
// Fetch BTC/ETH prices for the brief
|
||||||
var btcPrice, ethPrice, btcChg, ethChg string
|
var btcPrice, ethPrice, btcChg, ethChg string
|
||||||
for _, sym := range []string{"BTCUSDT", "ETHUSDT"} {
|
for _, sym := range []string{"BTCUSDT", "ETHUSDT"} {
|
||||||
resp, err := http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", sym))
|
resp, err := b.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", sym))
|
||||||
if err != nil { continue }
|
if err != nil { continue }
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, readErr := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
if readErr != nil { continue }
|
||||||
var t map[string]string
|
var t map[string]string
|
||||||
json.Unmarshal(body, &t)
|
if err := json.Unmarshal(body, &t); err != nil { continue }
|
||||||
if sym == "BTCUSDT" { btcPrice = t["lastPrice"]; btcChg = t["priceChangePercent"] }
|
if sym == "BTCUSDT" { btcPrice = t["lastPrice"]; btcChg = t["priceChangePercent"] }
|
||||||
if sym == "ETHUSDT" { ethPrice = t["lastPrice"]; ethChg = t["priceChangePercent"] }
|
if sym == "ETHUSDT" { ethPrice = t["lastPrice"]; ethChg = t["priceChangePercent"] }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,10 @@ func (s *Sentinel) check(symbol string) {
|
|||||||
resp, err := s.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
|
resp, err := s.http.Get(fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
|
||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil { return }
|
||||||
var t map[string]interface{}
|
var t map[string]interface{}
|
||||||
json.Unmarshal(body, &t)
|
if err := json.Unmarshal(body, &t); err != nil { return }
|
||||||
|
|
||||||
price, _ := strconv.ParseFloat(fmt.Sprint(t["lastPrice"]), 64)
|
price, _ := strconv.ParseFloat(fmt.Sprint(t["lastPrice"]), 64)
|
||||||
vol, _ := strconv.ParseFloat(fmt.Sprint(t["quoteVolume"]), 64)
|
vol, _ := strconv.ParseFloat(fmt.Sprint(t["quoteVolume"]), 64)
|
||||||
|
|||||||
@@ -247,7 +247,6 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create trader configuration (database entity)
|
// Create trader configuration (database entity)
|
||||||
logger.Infof("🔧 DEBUG: Starting to create trader config, ID=%s, Name=%s, AIModel=%s, Exchange=%s, StrategyID=%s", traderID, req.Name, req.AIModelID, req.ExchangeID, req.StrategyID)
|
|
||||||
traderRecord := &store.Trader{
|
traderRecord := &store.Trader{
|
||||||
ID: traderID,
|
ID: traderID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
@@ -271,24 +270,18 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save to database
|
// Save to database
|
||||||
logger.Infof("🔧 DEBUG: Preparing to call CreateTrader")
|
|
||||||
err = s.store.Trader().Create(traderRecord)
|
err = s.store.Trader().Create(traderRecord)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("❌ Failed to create trader: %v", err)
|
logger.Infof("❌ Failed to create trader: %v", err)
|
||||||
SafeInternalError(c, "Failed to create trader", err)
|
SafeInternalError(c, "Failed to create trader", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Infof("🔧 DEBUG: CreateTrader succeeded")
|
|
||||||
|
|
||||||
// Immediately load new trader into TraderManager
|
// Immediately load new trader into TraderManager
|
||||||
logger.Infof("🔧 DEBUG: Preparing to call LoadUserTraders")
|
|
||||||
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
err = s.traderManager.LoadUserTradersFromStore(s.store, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Infof("⚠️ Failed to load user traders into memory: %v", err)
|
logger.Infof("⚠️ Failed to load user traders into memory: %v", err)
|
||||||
// Don't return error here since trader was successfully created in database
|
// Don't return error here since trader was successfully created in database
|
||||||
}
|
}
|
||||||
logger.Infof("🔧 DEBUG: LoadUserTraders completed")
|
|
||||||
|
|
||||||
logger.Infof("✓ Trader created successfully: %s (model: %s, exchange: %s)", req.Name, req.AIModelID, req.ExchangeID)
|
logger.Infof("✓ Trader created successfully: %s (model: %s, exchange: %s)", req.Name, req.AIModelID, req.ExchangeID)
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
|||||||
@@ -46,26 +46,20 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
|||||||
var accountValue, totalMarginUsed float64
|
var accountValue, totalMarginUsed float64
|
||||||
var summaryType string
|
var summaryType string
|
||||||
var summary interface{}
|
var summary interface{}
|
||||||
|
_, _ = summaryType, summary
|
||||||
|
|
||||||
if t.isCrossMargin {
|
if t.isCrossMargin {
|
||||||
// Cross margin mode: use CrossMarginSummary
|
// Cross margin mode: use CrossMarginSummary
|
||||||
accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64)
|
||||||
totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64)
|
||||||
summaryType = "CrossMarginSummary (cross margin)"
|
summaryType = "CrossMarginSummary (cross margin)"
|
||||||
summary = accountState.CrossMarginSummary
|
|
||||||
} else {
|
} else {
|
||||||
// Isolated margin mode: use MarginSummary
|
// Isolated margin mode: use MarginSummary
|
||||||
accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64)
|
accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64)
|
||||||
totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64)
|
totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64)
|
||||||
summaryType = "MarginSummary (isolated margin)"
|
summaryType = "MarginSummary (isolated margin)"
|
||||||
summary = accountState.MarginSummary
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug: Print complete summary structure returned by API
|
|
||||||
summaryJSON, _ := json.MarshalIndent(summary, " ", " ")
|
|
||||||
logger.Infof("🔍 [DEBUG] Hyperliquid API %s complete data:", summaryType)
|
|
||||||
logger.Infof("%s", string(summaryJSON))
|
|
||||||
|
|
||||||
// Critical fix: Accumulate actual unrealized PnL from all positions
|
// Critical fix: Accumulate actual unrealized PnL from all positions
|
||||||
totalUnrealizedPnl := 0.0
|
totalUnrealizedPnl := 0.0
|
||||||
for _, assetPos := range accountState.AssetPositions {
|
for _, assetPos := range accountState.AssetPositions {
|
||||||
@@ -159,7 +153,7 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) {
|
|||||||
walletBalanceWithoutUnrealized,
|
walletBalanceWithoutUnrealized,
|
||||||
totalUnrealizedPnl)
|
totalUnrealizedPnl)
|
||||||
logger.Infof(" • Perpetuals available balance: %.2f USDC", availableBalance)
|
logger.Infof(" • Perpetuals available balance: %.2f USDC", availableBalance)
|
||||||
logger.Infof(" • Margin used: %.2f USDC", totalMarginUsed)
|
logger.Infof(" • Margin used: %.2f USDC (source: %s)", totalMarginUsed, summaryType)
|
||||||
logger.Infof(" • xyz dex equity: %.2f USDC (wallet %.2f + unrealized %.2f)",
|
logger.Infof(" • xyz dex equity: %.2f USDC (wallet %.2f + unrealized %.2f)",
|
||||||
xyzAccountValue,
|
xyzAccountValue,
|
||||||
xyzWalletBalance,
|
xyzWalletBalance,
|
||||||
|
|||||||
Reference in New Issue
Block a user