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 b1500f9e9a
commit 0f67747b0c
4 changed files with 61 additions and 19 deletions

View File

@@ -285,14 +285,20 @@ func (a *Agent) buildSystemPrompt(lang string) string {
%s
%s
## 数据说明
## 数据说明(极其重要,违反即失职!)
- 加密货币BTC/ETH等交易所实时数据标注 [Real-time]
- A股/港股/美股:通过 search_stock 工具获取**实时行情**(新浪财经数据源)
- 美股支持**盘前盘后数据**search_stock 返回的 quote 中 ext_price/ext_change_pct/ext_time 为盘前盘后价格
- 当返回数据包含 is_ext_hours=true 时,**必须**向用户展示盘前/盘后价格
- 外汇:没有实时数据,需要说明
- 对于没有实时数据的标的,严禁编造具体价格
- **有数据就给数据,没有才说没有。诚实但不过度声明。**
- A股/港股/美股:**必须调用 search_stock 工具**获取实时行情。不调工具就没有数据。
- 美股盘前盘后search_stock 返回的 quote 中 ext_price/ext_change_pct/ext_time
- 外汇/指数期货:当前没有数据源,如实告知
### 铁律:禁止编造任何价格
- **你的训练数据中的价格全部过时,不可使用**
- **没有通过工具获取的价格 = 你不知道 = 不能说**
- 用户问多只股票的盘前数据?→ 对每只股票调用 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
## Data Notice
## Data Notice (CRITICAL — violating this is unacceptable!)
- 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)
- US stocks include **pre-market/after-hours data**: ext_price/ext_change_pct/ext_time in quote
- When is_ext_hours=true, you MUST show pre-market/after-hours prices to the user
- Forex: No real-time data — must inform user
- NEVER fabricate prices for assets without data
- Show data when you have it. Only disclaim when you don't.
- Stocks: You MUST call search_stock tool to get real-time quotes. No tool call = no data.
- US stocks pre/after-hours: ext_price/ext_change_pct/ext_time in search_stock results
- Forex/Index futures: No data source currently — tell user honestly
### ABSOLUTE RULE: NEVER fabricate any price!
- 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
You can call these tools to take action:

View File

@@ -24,6 +24,7 @@ func (s *Scheduler) Start(ctx context.Context) {
defer ticker.Stop()
lastReport := time.Time{}
lastCheck := time.Time{}
lastCleanup := time.Time{}
for {
select {
@@ -40,6 +41,13 @@ func (s *Scheduler) Start(ctx context.Context) {
s.riskCheck()
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"
"log/slog"
"net/http"
"regexp"
"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.
type WebHandler struct {
agent *Agent
@@ -36,7 +43,8 @@ func (w *WebHandler) HandleChat(rw http.ResponseWriter, r *http.Request) {
UserID int64 `json:"user_id"`
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"})
return
}
@@ -70,6 +78,15 @@ func (w *WebHandler) HandleKlines(rw http.ResponseWriter, r *http.Request) {
interval := r.URL.Query().Get("interval")
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))
}
@@ -78,6 +95,11 @@ func (w *WebHandler) HandleTicker(rw http.ResponseWriter, r *http.Request) {
symbol := r.URL.Query().Get("symbol")
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))
}

View File

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