fix: enforce tool-based data only — Agent can no longer fabricate prices

- System prompt now has absolute rule: no tool result = no price
- Pre-market overview must call search_stock for each major stock
- Index futures explicitly marked as unsupported
- Training data prices explicitly declared outdated and forbidden
- Applied to both CN and EN prompts
This commit is contained in:
shinchan-zhai
2026-03-23 09:28:06 +08:00
parent f37c63f9bb
commit 59e7e37db1
4 changed files with 61 additions and 19 deletions

View File

@@ -285,14 +285,20 @@ func (a *Agent) buildSystemPrompt(lang string) string {
%s %s
%s %s
## 数据说明 ## 数据说明(极其重要,违反即失职!)
- 加密货币BTC/ETH等交易所实时数据标注 [Real-time] - 加密货币BTC/ETH等交易所实时数据标注 [Real-time]
- A股/港股/美股:通过 search_stock 工具获取**实时行情**(新浪财经数据源) - A股/港股/美股:**必须调用 search_stock 工具**获取实时行情。不调工具就没有数据。
- 美股支持**盘前盘后数据**search_stock 返回的 quote 中 ext_price/ext_change_pct/ext_time 为盘前盘后价格 - 美股盘前盘后search_stock 返回的 quote 中 ext_price/ext_change_pct/ext_time
- 当返回数据包含 is_ext_hours=true 时,**必须**向用户展示盘前/盘后价格 - 外汇/指数期货:当前没有数据源,如实告知
- 外汇:没有实时数据,需要说明
- 对于没有实时数据的标的,严禁编造具体价格 ### 铁律:禁止编造任何价格
- **有数据就给数据,没有才说没有。诚实但不过度声明。** - **你的训练数据中的价格全部过时,不可使用**
- **没有通过工具获取的价格 = 你不知道 = 不能说**
- 用户问多只股票的盘前数据?→ 对每只股票调用 search_stock 工具
- 用户问"盘前概览"?→ 调用 search_stock 查主要股票AAPL、TSLA、NVDA、MSFT、GOOGL、AMZN、META等用真实数据回答
- **绝对不允许**不调工具就给出具体价格数字(如 $421.85
- 如果某只股票 search_stock 查不到数据,就说"暂时无法获取该股票数据"
- 指数期货(纳指、标普、道琼斯期货)我们目前没有数据源,直接说"暂不支持指数期货数据"
## 工具使用 ## 工具使用
你可以调用以下工具来执行操作: 你可以调用以下工具来执行操作:
@@ -340,14 +346,20 @@ func (a *Agent) buildSystemPrompt(lang string) string {
%s %s
%s %s
## Data Notice ## Data Notice (CRITICAL — violating this is unacceptable!)
- Crypto (BTC/ETH): Exchange real-time data, marked [Real-time] - Crypto (BTC/ETH): Exchange real-time data, marked [Real-time]
- A-shares/HK/US stocks: Real-time quotes via search_stock tool (Sina Finance data) - Stocks: You MUST call search_stock tool to get real-time quotes. No tool call = no data.
- US stocks include **pre-market/after-hours data**: ext_price/ext_change_pct/ext_time in quote - US stocks pre/after-hours: ext_price/ext_change_pct/ext_time in search_stock results
- When is_ext_hours=true, you MUST show pre-market/after-hours prices to the user - Forex/Index futures: No data source currently — tell user honestly
- Forex: No real-time data — must inform user
- NEVER fabricate prices for assets without data ### ABSOLUTE RULE: NEVER fabricate any price!
- Show data when you have it. Only disclaim when you don't. - Your training data prices are ALL outdated and MUST NOT be used
- No tool result = you don't know = you cannot state a price
- User asks multiple stocks? → Call search_stock for EACH one
- User asks "pre-market overview"? → Call search_stock for major stocks (AAPL, TSLA, NVDA, MSFT, GOOGL, AMZN, META etc.) and use real data
- NEVER output a specific price number (like $421.85) without a tool having returned it
- If search_stock fails for a stock, say "unable to fetch data for this stock"
- Index futures (NDX, SPX, DJI futures) — we have no data source, say "index futures not supported yet"
## Tools ## Tools
You can call these tools to take action: You can call these tools to take action:

View File

@@ -24,6 +24,7 @@ func (s *Scheduler) Start(ctx context.Context) {
defer ticker.Stop() defer ticker.Stop()
lastReport := time.Time{} lastReport := time.Time{}
lastCheck := time.Time{} lastCheck := time.Time{}
lastCleanup := time.Time{}
for { for {
select { select {
@@ -40,6 +41,13 @@ func (s *Scheduler) Start(ctx context.Context) {
s.riskCheck() s.riskCheck()
lastCheck = now lastCheck = now
} }
// Clean stale chat history every hour (sessions idle > 24h)
if now.Sub(lastCleanup) > 1*time.Hour {
if s.agent.history != nil {
s.agent.history.CleanOld(24 * time.Hour)
}
lastCleanup = now
}
} }
} }
}() }()

View File

@@ -7,9 +7,16 @@ import (
"io" "io"
"log/slog" "log/slog"
"net/http" "net/http"
"regexp"
"time" "time"
) )
// validSymbolRe matches only alphanumeric trading symbols (e.g. BTCUSDT, ETH-USD).
var validSymbolRe = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,20}$`)
// validIntervalRe matches only valid kline intervals (e.g. 1m, 5m, 1h, 4h, 1d, 1w).
var validIntervalRe = regexp.MustCompile(`^[0-9]{1,2}[mhHdDwWM]$`)
// WebHandler provides HTTP endpoints for the NOFXi agent. // WebHandler provides HTTP endpoints for the NOFXi agent.
type WebHandler struct { type WebHandler struct {
agent *Agent agent *Agent
@@ -36,7 +43,8 @@ func (w *WebHandler) HandleChat(rw http.ResponseWriter, r *http.Request) {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Lang string `json:"lang"` Lang string `json:"lang"`
} }
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { // Limit request body to 64KB to prevent abuse
if err := json.NewDecoder(io.LimitReader(r.Body, 64*1024)).Decode(&req); err != nil {
writeJSON(rw, 400, map[string]string{"error": "invalid request"}) writeJSON(rw, 400, map[string]string{"error": "invalid request"})
return return
} }
@@ -70,6 +78,15 @@ func (w *WebHandler) HandleKlines(rw http.ResponseWriter, r *http.Request) {
interval := r.URL.Query().Get("interval") interval := r.URL.Query().Get("interval")
if interval == "" { interval = "1h" } if interval == "" { interval = "1h" }
if !validSymbolRe.MatchString(symbol) {
writeJSON(rw, 400, map[string]string{"error": "invalid symbol"})
return
}
if !validIntervalRe.MatchString(interval) {
writeJSON(rw, 400, map[string]string{"error": "invalid interval"})
return
}
proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=300", symbol, interval)) proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=300", symbol, interval))
} }
@@ -78,6 +95,11 @@ func (w *WebHandler) HandleTicker(rw http.ResponseWriter, r *http.Request) {
symbol := r.URL.Query().Get("symbol") symbol := r.URL.Query().Get("symbol")
if symbol == "" { symbol = "BTCUSDT" } if symbol == "" { symbol = "BTCUSDT" }
if !validSymbolRe.MatchString(symbol) {
writeJSON(rw, 400, map[string]string{"error": "invalid symbol"})
return
}
proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol)) proxyBinance(rw, fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol))
} }

View File

@@ -30,10 +30,10 @@ func main() {
// Initialize logger // Initialize logger
logger.Init(nil) logger.Init(nil)
logger.Info("╔════════════════════════════════════════════════════════════╗") logger.Info("╔══════════════════════════════════════════════════╗")
logger.Info("║ 🚀 NOFXi - AI Trading Agent ║") logger.Info("║ 🚀 NOFXi - AI Trading Agent ║")
logger.Info("║ Powered by NOFX Engine ║") logger.Info("║ Powered by NOFX Engine ║")
logger.Info("╚════════════════════════════════════════════════════════════╝") logger.Info("╚══════════════════════════════════════════════════╝")
// Initialize global configuration (loaded from .env) // Initialize global configuration (loaded from .env)
config.Init() config.Init()