diff --git a/nofxi/docs/nofx-integration-plan.md b/docs/nofx-integration-plan.md similarity index 100% rename from nofxi/docs/nofx-integration-plan.md rename to docs/nofx-integration-plan.md diff --git a/nofxi/.gitignore b/nofxi/.gitignore deleted file mode 100644 index e37e8ae6..00000000 --- a/nofxi/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -# Binary -nofxi - -# Config (contains secrets) -config.yaml - -# Database -*.db -*.db-shm -*.db-wal - -# Logs -*.log diff --git a/nofxi/Dockerfile b/nofxi/Dockerfile deleted file mode 100644 index a77b88b1..00000000 --- a/nofxi/Dockerfile +++ /dev/null @@ -1,31 +0,0 @@ -# Build stage -FROM golang:1.25-alpine AS builder - -RUN apk add --no-cache git - -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /nofxi ./nofxi/cmd/nofxi/ - -# Runtime stage -FROM alpine:3.19 - -RUN apk add --no-cache ca-certificates tzdata - -WORKDIR /app - -COPY --from=builder /nofxi . -COPY nofxi/config.example.yaml ./config.example.yaml - -# Create data directory for SQLite -RUN mkdir -p /app/data - -ENV TZ=Asia/Shanghai - -EXPOSE 8900 - -ENTRYPOINT ["./nofxi"] -CMD ["--config", "/app/config.yaml"] diff --git a/nofxi/LICENSE b/nofxi/LICENSE deleted file mode 100644 index 673f6e47..00000000 --- a/nofxi/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Proprietary License - -Copyright (c) 2026 NoFxAiOS. All rights reserved. - -This software and associated documentation files (the "Software") are the -proprietary property of NoFxAiOS. No part of this Software may be reproduced, -distributed, or transmitted in any form or by any means without the prior -written permission of NoFxAiOS. - -Unauthorized copying, modification, distribution, or use of this Software, -via any medium, is strictly prohibited. - -For licensing inquiries, contact: https://github.com/NoFxAiOS diff --git a/nofxi/README.md b/nofxi/README.md deleted file mode 100644 index 58fd9013..00000000 --- a/nofxi/README.md +++ /dev/null @@ -1,143 +0,0 @@ -

NOFXi

- -

- Your AI Trading Agent.
- Not a tool. A partner that trades with you. -

- -

- Go - React - x402 -

- -

- English · - 中文 -

- ---- - - - -## What is NOFXi? - -NOFXi is an **AI Trading Agent** built on top of [NOFX](https://github.com/NoFxAiOS/nofx). While NOFX is an open-source AI trading engine, NOFXi elevates it into a fully autonomous agent that perceives, thinks, remembers, and acts. - -**NOFX** = Engine. **NOFXi** = Intelligence. - -### The Difference - -| | NOFX (Engine) | NOFXi (Agent) | -|:--|:--|:--| -| **Interaction** | Configure → Start → Watch Dashboard | "Open a BTC long, 2x leverage, 3% stop loss" | -| **Intelligence** | AI makes trade decisions | AI understands context, learns from history | -| **Memory** | Stateless per session | Remembers your preferences, past trades, lessons | -| **Proactivity** | Executes when triggered | Monitors markets, alerts you, acts autonomously | -| **Communication** | Dashboard & logs | Natural language via Telegram, proactive notifications | - -### Core Capabilities - -- 🧠 **Agent Core** — Context-aware conversation, intent recognition, memory management -- 👁️ **Perception** — Market monitoring, anomaly detection, position tracking, sentiment analysis -- 💭 **Thinking** — Multi-model AI decisions, strategy matching, risk assessment -- 📝 **Memory** — Trade history, strategy performance, user preferences, market patterns -- ⚡ **Execution** — Multi-exchange trading, position management, x402 payments -- 💬 **Interaction** — Natural language trading, proactive alerts, report generation - -### Architecture - -``` -Interaction Layer (Telegram / Web UI / API) - │ - Agent Core (Conversation + Intent + Context) - │ - ┌────┴───────────┬────────────┐ - │ │ │ -Perception Thinking Memory -(Market Monitor) (AI Engine) (Experience DB) - │ │ │ - └────┬───────────┴────────────┘ - │ - Execution Layer (Multi-Exchange + x402 Payment) -``` - -### Built On - -- **[NOFX](https://github.com/NoFxAiOS/nofx)** — Open-source AI trading engine (9 exchanges, 7+ AI models) -- **[Claw402](https://claw402.ai)** — x402 payment gateway (USDC micropayments) -- **[x402](https://x402.org)** — Pay-per-request with USDC, no API keys - ---- - - - -## NOFXi 是什么? - -NOFXi 是基于 [NOFX](https://github.com/NoFxAiOS/nofx) 构建的 **AI 交易 Agent**。NOFX 是开源的 AI 交易引擎,而 NOFXi 将其升级为一个能感知、思考、记忆、行动的自主 Agent。 - -**NOFX** = 引擎。**NOFXi** = 智能。 - -### 核心区别 - -| | NOFX(引擎) | NOFXi(Agent) | -|:--|:--|:--| -| **交互方式** | 配置 → 启动 → 看仪表盘 | "帮我开 BTC 多单,2x 杠杆,3% 止损" | -| **智能程度** | AI 做交易决策 | AI 理解上下文,从历史中学习 | -| **记忆** | 每次无状态 | 记住你的偏好、交易历史、教训 | -| **主动性** | 被触发时执行 | 主动监控市场、通知你、自主行动 | -| **沟通方式** | 仪表盘和日志 | Telegram 自然语言对话、主动推送 | - -### 核心能力 - -- 🧠 **Agent 核心** — 上下文对话、意图识别、记忆管理 -- 👁️ **感知层** — 市场监控、异动检测、持仓监控、舆情分析 -- 💭 **思考层** — 多模型 AI 决策、策略匹配、风险评估 -- 📝 **记忆层** — 交易历史、策略效果、用户偏好、市场规律 -- ⚡ **执行层** — 多交易所下单、仓位管理、x402 支付 -- 💬 **交互层** — 自然语言交易、主动通知、报告生成 - -### 技术架构 - -``` -交互层(Telegram / Web UI / API) - │ - Agent Core(对话理解 + 意图识别 + 上下文管理) - │ - ┌────┴───────────┬────────────┐ - │ │ │ - 感知层 思考层 记忆层 -(市场监控) (AI 决策引擎) (经验库) - │ │ │ - └────┬───────────┴────────────┘ - │ - 执行层(多交易所 + x402 支付) -``` - -### 构建基础 - -- **[NOFX](https://github.com/NoFxAiOS/nofx)** — 开源 AI 交易引擎(9 家交易所,7+ AI 模型) -- **[Claw402](https://claw402.ai)** — x402 支付网关(USDC 微支付) -- **[x402](https://x402.org)** — USDC 按量付费,无需 API Key - ---- - -## Development - -```bash -# Clone -git clone https://github.com/NoFxAiOS/nofxi.git -cd nofxi - -# Build -go build ./... - -# Run -go run cmd/nofxi/main.go -``` - -## License - -Proprietary. All rights reserved. - -© 2026 NoFxAiOS diff --git a/nofxi/config.example.yaml b/nofxi/config.example.yaml deleted file mode 100644 index 2d70f424..00000000 --- a/nofxi/config.example.yaml +++ /dev/null @@ -1,35 +0,0 @@ -# NOFXi Configuration -# Copy to config.yaml and fill in your values - -agent: - name: "NOFXi" - language: "zh" # "en" or "zh" - log_level: "info" # "debug", "info", "warn", "error" - web_port: 8900 # REST API port (0 = disabled) - -telegram: - token: "YOUR_TELEGRAM_BOT_TOKEN" - allowed_ids: # Leave empty to allow all users - # - 123456789 - -llm: - provider: "claw402" # "openai", "claw402", "deepseek" - base_url: "https://claw402.ai/v1" - api_key: "YOUR_API_KEY" - model: "deepseek-chat" - -database: - path: "nofxi.db" - -exchanges: - - name: "binance" - api_key: "" - api_secret: "" - testnet: true - # - name: "okx" - # api_key: "" - # api_secret: "" - # passphrase: "" - # - name: "bybit" - # api_key: "" - # api_secret: "" diff --git a/nofxi/docker-compose.yml b/nofxi/docker-compose.yml deleted file mode 100644 index 4c502025..00000000 --- a/nofxi/docker-compose.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: '3.8' - -services: - nofxi: - build: - context: .. - dockerfile: nofxi/Dockerfile - container_name: nofxi - restart: unless-stopped - ports: - - "8900:8900" - volumes: - - ./config.yaml:/app/config.yaml:ro - - nofxi-data:/app/data - environment: - - TZ=Asia/Shanghai - # Override config via env vars: - # - NOFXI_TELEGRAM_TOKEN=xxx - # - NOFXI_LLM_API_KEY=xxx - # - NOFXI_LLM_BASE_URL=xxx - -volumes: - nofxi-data: diff --git a/nofxi/internal/agent/agent.go b/nofxi/internal/agent/agent.go deleted file mode 100644 index 8ffddc28..00000000 --- a/nofxi/internal/agent/agent.go +++ /dev/null @@ -1,396 +0,0 @@ -// Package agent implements the NOFXi Agent Core. -package agent - -import ( - "context" - "fmt" - "log/slog" - "strconv" - "strings" - "time" - - "nofx/nofxi/internal/execution" - "nofx/nofxi/internal/memory" - "nofx/nofxi/internal/perception" - "nofx/nofxi/internal/thinking" -) - -// Agent is the NOFXi agent core. -type Agent struct { - config *Config - memory *memory.Store - thinker thinking.Engine - bridge *execution.Bridge - monitor *perception.MarketMonitor - logger *slog.Logger - NotifyFunc func(userID int64, text string) error - strategyRunner *StrategyRunner -} - -func New(cfg *Config, mem *memory.Store, thinker thinking.Engine, logger *slog.Logger) *Agent { - a := &Agent{config: cfg, memory: mem, thinker: thinker, logger: logger} - a.strategyRunner = NewStrategyRunner(a, logger) - return a -} - -func (a *Agent) SetBridge(b *execution.Bridge) { a.bridge = b } -func (a *Agent) SetMonitor(m *perception.MarketMonitor) { a.monitor = m } - -func (a *Agent) getLang(userID int64) string { - l, _ := a.memory.GetPreference(userID, "lang") - if l == "" { - l = a.config.Agent.Language - } - if l != "zh" && l != "en" { - l = "en" - } - return l -} - -// HandleMessage processes a user message and returns a response. -func (a *Agent) HandleMessage(ctx context.Context, userID int64, text string) (string, error) { - // Extract lang prefix from web UI - if strings.HasPrefix(text, "[lang:") { - if end := strings.Index(text, "] "); end > 0 { - lang := text[6:end] - if lang == "zh" || lang == "en" { - a.memory.SetPreference(userID, "lang", lang) - } - text = text[end+2:] - } - } - - a.logger.Info("incoming message", "user_id", userID, "text", text) - a.memory.SaveMessage(userID, "user", text) - - intent := Route(text) - a.logger.Info("routed intent", "type", intent.Type, "params", intent.Params) - - L := a.getLang(userID) - var resp string - var err error - - switch intent.Type { - case IntentHelp: - resp = msg(L, "help") - case IntentStatus: - resp = a.handleStatus(L) - case IntentQuery: - resp, err = a.handleQuery(ctx, L, intent) - case IntentAnalyze: - resp, err = a.handleAnalyze(ctx, L, intent) - case IntentTrade: - resp, err = a.handleTrade(ctx, userID, L, intent) - case IntentWatch: - resp = a.HandleWatchCommand(intent.Raw) - case IntentStrategy: - resp = a.handleStrategyCommand(intent.Raw) - case IntentSettings: - resp = fmt.Sprintf(msg(L, "settings"), L, a.config.LLM.Model, a.config.LLM.Provider, len(a.config.Exchanges)) - default: - resp, err = a.handleChat(ctx, userID, L, text) - } - - if err != nil { - a.logger.Error("handle message", "intent", intent.Type, "error", err) - resp = fmt.Sprintf("⚠️ Error: %v", err) - } - - a.memory.SaveMessage(userID, "assistant", resp) - return resp, nil -} - -func (a *Agent) handleStatus(L string) string { - wc := 0 - if a.monitor != nil { - wc = len(a.monitor.GetAllSnapshots()) - } - bs := msg(L, "bridge_disconnected") - if a.bridge != nil { - bs = msg(L, "bridge_connected") - } - return fmt.Sprintf(msg(L, "status_title"), - a.config.Agent.Name, a.config.LLM.Model, a.config.LLM.Provider, - bs, wc, time.Now().Format("2006-01-02 15:04:05")) -} - -func (a *Agent) handleQuery(ctx context.Context, L string, intent Intent) (string, error) { - raw := strings.ToLower(intent.Raw) - - if a.bridge != nil && (strings.Contains(raw, "position") || strings.Contains(raw, "持仓")) { - return a.queryPositions(L) - } - if a.bridge != nil && (strings.Contains(raw, "balance") || strings.Contains(raw, "余额")) { - return a.queryBalance(L) - } - - trades, err := a.memory.GetRecentTrades(10) - if err != nil { - return "", fmt.Errorf("get trades: %w", err) - } - if len(trades) == 0 { - return msg(L, "no_trades"), nil - } - - var sb strings.Builder - sb.WriteString(msg(L, "recent_trades")) - total := 0.0 - for _, t := range trades { - e := "🟢" - if t.PnL < 0 { - e = "🔴" - } - sb.WriteString(fmt.Sprintf("%s %s %s %s — $%.2f (P/L: $%.2f)\n", - e, t.Side, t.Symbol, t.Exchange, t.Price*t.Quantity, t.PnL)) - total += t.PnL - } - sb.WriteString(fmt.Sprintf(msg(L, "total_pnl"), total)) - return sb.String(), nil -} - -func (a *Agent) queryPositions(L string) (string, error) { - var all []execution.Position - for _, ex := range a.config.Exchanges { - pos, err := a.bridge.GetPositions(ex.Name) - if err != nil { - continue - } - all = append(all, pos...) - } - if len(all) == 0 { - return msg(L, "no_positions"), nil - } - - var sb strings.Builder - sb.WriteString(msg(L, "open_positions")) - total := 0.0 - for _, p := range all { - e := "🟢" - if p.PnL < 0 { - e = "🔴" - } - sb.WriteString(fmt.Sprintf("%s *%s* %s\n Size: %.4f | Entry: $%.2f\n Mark: $%.2f | P/L: $%.2f\n", - e, p.Symbol, strings.ToUpper(p.Side), p.Size, p.EntryPrice, p.MarkPrice, p.PnL)) - if p.Leverage > 0 { - sb.WriteString(fmt.Sprintf(" Leverage: %.0fx | Exchange: %s\n", p.Leverage, p.Exchange)) - } - sb.WriteString("\n") - total += p.PnL - } - sb.WriteString(fmt.Sprintf(msg(L, "total_unrealized"), total)) - return sb.String(), nil -} - -func (a *Agent) queryBalance(L string) (string, error) { - var sb strings.Builder - sb.WriteString(msg(L, "account_balance")) - for _, ex := range a.config.Exchanges { - bal, err := a.bridge.GetBalance(ex.Name) - if err != nil { - sb.WriteString(fmt.Sprintf("• %s: ⚠️ Error\n", ex.Name)) - continue - } - sb.WriteString(fmt.Sprintf("*%s*\n", ex.Name)) - sb.WriteString(fmt.Sprintf(msg(L, "balance_total"), bal.Total)) - sb.WriteString(fmt.Sprintf(msg(L, "balance_available"), bal.Available)) - sb.WriteString(fmt.Sprintf(msg(L, "balance_in_position"), bal.InPosition)) - } - return sb.String(), nil -} - -func (a *Agent) handleAnalyze(ctx context.Context, L string, intent Intent) (string, error) { - symbol := "BTC" - if d, ok := intent.Params["detail"]; ok && d != "" { - symbol = strings.ToUpper(strings.TrimSpace(d)) - } - - priceInfo := "" - if a.monitor != nil { - if snap, ok := a.monitor.GetSnapshot(symbol + "USDT"); ok && snap.LastPrice > 0 { - priceInfo = fmt.Sprintf("\nCurrent price: $%.2f", snap.LastPrice) - } - } - - prompt := fmt.Sprintf("Analyze %s/USDT for trading. %s\nConsider: trend, support/resistance, momentum, volume, sentiment.\nGive specific entry/exit levels and stop loss. Be concise. Respond in %s.", - symbol, priceInfo, map[string]string{"zh": "Chinese", "en": "English"}[L]) - - analysis, err := a.thinker.Analyze(ctx, prompt) - if err != nil { - return "", fmt.Errorf("AI analyze: %w", err) - } - - emojiMap := map[string]string{"buy": "🟢 BUY", "sell": "🔴 SELL", "hold": "🟡 HOLD", "wait": "⏳ WAIT"} - action := emojiMap[analysis.Action] - if action == "" { - action = "🤔 " + analysis.Action - } - - result := fmt.Sprintf(msg(L, "analysis_signal"), symbol, action, analysis.Confidence*100, analysis.Reasoning) - if analysis.StopLoss > 0 { - result += fmt.Sprintf(msg(L, "stop_loss"), analysis.StopLoss) - } - if analysis.TakeProfit > 0 { - result += fmt.Sprintf(msg(L, "take_profit"), analysis.TakeProfit) - } - return result, nil -} - -func (a *Agent) handleTrade(ctx context.Context, userID int64, L string, intent Intent) (string, error) { - action := strings.ToLower(intent.Params["action"]) - detail := intent.Params["detail"] - - if a.bridge == nil || len(a.config.Exchanges) == 0 { - return msg(L, "no_exchange"), nil - } - - parts := strings.Fields(detail) - if len(parts) < 1 { - return msg(L, "trade_usage"), nil - } - - symbol := strings.ToUpper(parts[0]) - if !strings.HasSuffix(symbol, "USDT") { - symbol += "USDT" - } - - quantity := 0.0 - leverage := 1 - if len(parts) >= 2 { - q, err := strconv.ParseFloat(parts[1], 64) - if err != nil { - return fmt.Sprintf(msg(L, "invalid_quantity"), parts[1]), nil - } - quantity = q - } - if len(parts) >= 3 { - if l, err := strconv.Atoi(strings.TrimSuffix(strings.ToLower(parts[2]), "x")); err == nil { - leverage = l - } - } - - if quantity <= 0 { - return msg(L, "specify_quantity"), nil - } - - var side string - switch action { - case "buy", "long", "open_long", "做多": - side = "LONG" - case "sell", "short", "open_short", "做空": - side = "SHORT" - case "close", "平仓": - side = "CLOSE_LONG" - default: - side = strings.ToUpper(action) - } - - exchange := a.config.Exchanges[0].Name - a.memory.SetPreference(userID, "pending_trade", - fmt.Sprintf("%s|%s|%f|%d|%s", side, symbol, quantity, leverage, exchange)) - - return fmt.Sprintf(msg(L, "confirm_trade"), side, symbol, quantity, leverage, exchange), nil -} - -// ExecutePendingTrade executes a confirmed trade. -func (a *Agent) ExecutePendingTrade(ctx context.Context, userID int64, L string) (string, error) { - pending, err := a.memory.GetPreference(userID, "pending_trade") - if err != nil || pending == "" { - return "", fmt.Errorf(msg(L, "no_pending")) - } - a.memory.SetPreference(userID, "pending_trade", "") - - parts := strings.Split(pending, "|") - if len(parts) != 5 { - return "", fmt.Errorf("invalid pending trade") - } - - side, symbol := parts[0], parts[1] - quantity, _ := strconv.ParseFloat(parts[2], 64) - leverage, _ := strconv.Atoi(parts[3]) - exchange := parts[4] - - result, err := a.bridge.PlaceOrder(exchange, symbol, side, quantity, leverage) - if err != nil { - return "", fmt.Errorf("execute trade: %w", err) - } - - tr := &memory.TradeRecord{Exchange: exchange, Symbol: symbol, Side: strings.ToLower(side), Type: "market", Quantity: quantity, Status: "open"} - if p, ok := result["avgPrice"].(float64); ok { - tr.Price = p - } - a.memory.SaveTrade(tr) - - return fmt.Sprintf(msg(L, "trade_executed"), side, symbol, quantity, leverage, exchange, result), nil -} - -func (a *Agent) handleStrategyCommand(text string) string { - parts := strings.Fields(text) - if len(parts) < 2 { - return a.strategyRunner.FormatStrategyList() - } - switch strings.ToLower(parts[1]) { - case "list": - return a.strategyRunner.FormatStrategyList() - case "start": - if len(parts) < 3 { - return "Usage: `/strategy start BTC 1h`" - } - sym := strings.ToUpper(parts[2]) - if !strings.HasSuffix(sym, "USDT") { - sym += "USDT" - } - iv := 1 * time.Hour - if len(parts) >= 4 { - switch parts[3] { - case "15m": - iv = 15 * time.Minute - case "30m": - iv = 30 * time.Minute - case "4h": - iv = 4 * time.Hour - } - } - ex := "binance" - if len(parts) >= 5 { - ex = parts[4] - } - id, err := a.strategyRunner.StartStrategy("AI-"+sym, sym, ex, iv) - if err != nil { - return fmt.Sprintf("⚠️ %v", err) - } - return fmt.Sprintf("🚀 Strategy started!\n\n• ID: `%s`\n• Symbol: %s\n• Interval: %s\n• Exchange: %s", id, sym, iv, ex) - case "stop": - if len(parts) < 3 { - return "Usage: `/strategy stop `" - } - if err := a.strategyRunner.StopStrategy(parts[2]); err != nil { - return fmt.Sprintf("⚠️ %v", err) - } - return "✅ Strategy stopped." - case "stopall": - a.strategyRunner.StopAll() - return "✅ All strategies stopped." - default: - return "Use: `/strategy list|start|stop|stopall`" - } -} - -func (a *Agent) handleChat(ctx context.Context, userID int64, L string, text string) (string, error) { - lower := strings.ToLower(text) - if lower == "yes" || lower == "y" || lower == "确认" || lower == "是" { - if p, _ := a.memory.GetPreference(userID, "pending_trade"); p != "" { - return a.ExecutePendingTrade(ctx, userID, L) - } - } - - history, _ := a.memory.GetRecentMessages(userID, 20) - - sysPrompt := fmt.Sprintf(msg(L, "system_prompt"), time.Now().Format("2006-01-02 15:04:05")) - msgs := []thinking.Message{{Role: "system", Content: sysPrompt}} - for _, m := range history { - msgs = append(msgs, thinking.Message{Role: m.Role, Content: m.Content}) - } - msgs = append(msgs, thinking.Message{Role: "user", Content: text}) - - return a.thinker.Chat(ctx, msgs) -} diff --git a/nofxi/internal/agent/brain.go b/nofxi/internal/agent/brain.go deleted file mode 100644 index 9ad5780c..00000000 --- a/nofxi/internal/agent/brain.go +++ /dev/null @@ -1,189 +0,0 @@ -package agent - -import ( - "context" - "fmt" - "log/slog" - "strings" - "time" - - "nofx/nofxi/internal/perception" - "nofx/nofxi/internal/thinking" -) - -// Brain is the proactive intelligence layer. -// It receives signals from Sentinel, processes news, and decides -// when to proactively notify the user. -type Brain struct { - agent *Agent - news *perception.NewsMonitor - logger *slog.Logger - stopCh chan struct{} - - // Debounce: don't spam the same signal - recentSignals map[string]time.Time -} - -// NewBrain creates the proactive brain. -func NewBrain(agent *Agent, logger *slog.Logger) *Brain { - return &Brain{ - agent: agent, - news: perception.NewNewsMonitor(logger), - logger: logger, - stopCh: make(chan struct{}), - recentSignals: make(map[string]time.Time), - } -} - -// HandleSignal processes a market signal from the Sentinel. -func (b *Brain) HandleSignal(signal perception.Signal) { - // Debounce: same signal type + symbol within 10 minutes - key := fmt.Sprintf("%s:%s", signal.Type, signal.Symbol) - if last, ok := b.recentSignals[key]; ok && time.Since(last) < 10*time.Minute { - return - } - b.recentSignals[key] = time.Now() - - // Format alert message - emoji := map[string]string{ - "info": "ℹ️", - "warning": "⚠️", - "critical": "🚨", - } - e := emoji[signal.Severity] - if e == "" { - e = "📊" - } - - msg := fmt.Sprintf("%s *%s*\n\n%s\n\n_%s_", - e, signal.Title, signal.Detail, signal.Timestamp.Format("15:04:05")) - - b.notifyAll(msg) -} - -// StartNewsScan begins periodic news scanning. -func (b *Brain) StartNewsScan(interval time.Duration) { - go func() { - ticker := time.NewTicker(interval) - defer ticker.Stop() - - for { - select { - case <-b.stopCh: - return - case <-ticker.C: - b.scanNews() - } - } - }() -} - -// StartMarketBrief sends a morning/evening market brief. -func (b *Brain) StartMarketBrief() { - go func() { - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - for { - select { - case <-b.stopCh: - return - case now := <-ticker.C: - hour := now.Hour() - minute := now.Minute() - - // Morning brief at 08:30 - if hour == 8 && minute == 30 { - b.sendMarketBrief("morning") - } - // Evening brief at 20:30 - if hour == 20 && minute == 30 { - b.sendMarketBrief("evening") - } - } - } - }() -} - -func (b *Brain) scanNews() { - items, err := b.news.FetchNews() - if err != nil { - b.logger.Error("fetch news", "error", err) - return - } - - // Filter for high-impact news related to watched symbols - for _, item := range items { - if item.Sentiment == "neutral" { - continue - } - if len(item.Symbols) == 0 { - continue - } - // Only alert on recent news (last 10 minutes) - if time.Since(item.Timestamp) > 10*time.Minute { - continue - } - - emoji := "📰" - if item.Sentiment == "bullish" { - emoji = "🟢" - } else if item.Sentiment == "bearish" { - emoji = "🔴" - } - - msg := fmt.Sprintf("%s *%s*\n\n%s\n\n• Source: %s\n• Symbols: %s\n• Sentiment: %s", - emoji, "News Alert", - item.Title, - item.Source, - strings.Join(item.Symbols, ", "), - strings.ToUpper(item.Sentiment), - ) - b.notifyAll(msg) - } -} - -func (b *Brain) sendMarketBrief(timeOfDay string) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - prompt := fmt.Sprintf(`Generate a brief %s market summary for crypto trading. -Include: BTC/ETH price direction, key levels, market sentiment, any notable events. -Be concise (under 200 words). Use trading emojis. Respond in Chinese. -Current time: %s`, timeOfDay, time.Now().Format("2006-01-02 15:04:05")) - - resp, err := b.agent.thinker.Chat(ctx, []thinking.Message{ - {Role: "system", Content: "You are NOFXi, a professional crypto trading AI. Respond in Chinese."}, - {Role: "user", Content: prompt}, - }) - if err != nil { - b.logger.Error("generate market brief", "error", err) - return - } - - title := "☀️ *早间市场简报*" - if timeOfDay == "evening" { - title = "🌙 *晚间市场简报*" - } - - msg := fmt.Sprintf("%s\n\n%s\n\n_Generated by NOFXi 🤖_", title, resp) - b.notifyAll(msg) -} - -func (b *Brain) notifyAll(text string) { - if b.agent.NotifyFunc == nil { - return - } - for _, uid := range b.agent.config.Telegram.AllowedIDs { - if err := b.agent.NotifyFunc(uid, text); err != nil { - b.logger.Error("notify", "user_id", uid, "error", err) - } - } -} - -// Stop stops the brain. -func (b *Brain) Stop() { - close(b.stopCh) -} - - diff --git a/nofxi/internal/agent/config.go b/nofxi/internal/agent/config.go deleted file mode 100644 index f24855ec..00000000 --- a/nofxi/internal/agent/config.go +++ /dev/null @@ -1,98 +0,0 @@ -package agent - -import ( - "fmt" - "os" - - "gopkg.in/yaml.v3" -) - -// Config holds the complete NOFXi configuration. -type Config struct { - Agent AgentConfig `yaml:"agent"` - Telegram TelegramConfig `yaml:"telegram"` - LLM LLMConfig `yaml:"llm"` - Database DatabaseConfig `yaml:"database"` - Exchanges []ExchangeConfig `yaml:"exchanges"` -} - -type AgentConfig struct { - Name string `yaml:"name"` // Agent display name - Language string `yaml:"language"` // "en" or "zh" - LogLevel string `yaml:"log_level"` // "debug", "info", "warn", "error" - WebPort int `yaml:"web_port"` // REST API port (0 = disabled) -} - -type TelegramConfig struct { - Token string `yaml:"token"` - AllowedIDs []int64 `yaml:"allowed_ids"` // Allowed Telegram user IDs (empty = allow all) -} - -type LLMConfig struct { - Provider string `yaml:"provider"` // "openai", "claw402" - BaseURL string `yaml:"base_url"` // API base URL - APIKey string `yaml:"api_key"` - Model string `yaml:"model"` // e.g. "gpt-4o", "deepseek-chat" -} - -type DatabaseConfig struct { - Path string `yaml:"path"` // SQLite file path -} - -type ExchangeConfig struct { - Name string `yaml:"name"` // "binance", "okx", "bybit", etc. - APIKey string `yaml:"api_key"` - APISecret string `yaml:"api_secret"` - Passphrase string `yaml:"passphrase"` // OKX needs this - Testnet bool `yaml:"testnet"` -} - -// LoadConfig reads config from a YAML file. -func LoadConfig(path string) (*Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("read config: %w", err) - } - - cfg := &Config{ - Agent: AgentConfig{ - Name: "NOFXi", - Language: "en", - LogLevel: "info", - }, - Database: DatabaseConfig{ - Path: "nofxi.db", - }, - } - - if err := yaml.Unmarshal(data, cfg); err != nil { - return nil, fmt.Errorf("parse config: %w", err) - } - - // Override with env vars if set - if v := os.Getenv("NOFXI_TELEGRAM_TOKEN"); v != "" { - cfg.Telegram.Token = v - } - if v := os.Getenv("NOFXI_LLM_API_KEY"); v != "" { - cfg.LLM.APIKey = v - } - if v := os.Getenv("NOFXI_LLM_BASE_URL"); v != "" { - cfg.LLM.BaseURL = v - } - - return cfg, nil -} - -// Validate checks required fields. -func (c *Config) Validate() error { - if c.Telegram.Token == "" { - return fmt.Errorf("telegram.token is required") - } - if c.LLM.APIKey == "" { - return fmt.Errorf("llm.api_key is required") - } - if c.LLM.Model == "" { - return fmt.Errorf("llm.model is required") - } - return nil -} diff --git a/nofxi/internal/agent/i18n.go b/nofxi/internal/agent/i18n.go deleted file mode 100644 index 76167e7a..00000000 --- a/nofxi/internal/agent/i18n.go +++ /dev/null @@ -1,204 +0,0 @@ -package agent - -// i18n message templates -var messages = map[string]map[string]string{ - "help": { - "zh": `🤖 *NOFXi — 你的 AI 交易 Agent* - -*交易:* -/buy BTC 0.01 — 做多(市价单) -/sell BTC 0.01 — 做空 -/close BTC — 平仓 -/positions — 查看持仓 -/balance — 查看余额 -/pnl — 盈亏记录 - -*分析:* -/analyze BTC — AI 市场分析 -/watch BTC — 监控价格 -/alert BTC above 100000 — 价格提醒 -/price BTC — 实时价格 - -*策略:* -/strategy start BTC 1h — 启动 AI 自动策略 -/strategy list — 查看运行中的策略 -/strategy stop — 停止策略 - -*系统:* -/status — Agent 状态 -/help — 帮助菜单 - -直接跟我说话就行,中英文都可以 💬`, - - "en": `🤖 *NOFXi — Your AI Trading Agent* - -*Trading:* -/buy BTC 0.01 — Open long (market order) -/sell BTC 0.01 — Open short -/close BTC — Close position -/positions — View open positions -/balance — Check balance -/pnl — P/L history - -*Analysis:* -/analyze BTC — AI market analysis -/watch BTC — Monitor price -/alert BTC above 100000 — Price alert -/price BTC — Real-time price - -*Strategy:* -/strategy start BTC 1h — Start AI auto strategy -/strategy list — View active strategies -/strategy stop — Stop strategy - -*System:* -/status — Agent status -/help — This menu - -Just talk to me in any language 💬`, - }, - - "no_trades": { - "zh": "📭 暂无交易记录。试试 `/buy BTC 0.01` 或 `/analyze BTC` 开始吧。", - "en": "📭 No trades yet. Start with `/buy BTC 0.01` or `/analyze BTC`.", - }, - "no_positions": { - "zh": "📭 当前没有持仓。", - "en": "📭 No open positions.", - }, - "recent_trades": { - "zh": "📋 *最近交易*\n\n", - "en": "📋 *Recent Trades*\n\n", - }, - "total_pnl": { - "zh": "\n💰 总盈亏: $%.2f", - "en": "\n💰 Total P/L: $%.2f", - }, - "open_positions": { - "zh": "📊 *当前持仓*\n\n", - "en": "📊 *Open Positions*\n\n", - }, - "total_unrealized": { - "zh": "💰 *未实现总盈亏: $%.2f*", - "en": "💰 *Total Unrealized P/L: $%.2f*", - }, - "account_balance": { - "zh": "💰 *账户余额*\n\n", - "en": "💰 *Account Balance*\n\n", - }, - "balance_total": { - "zh": " 总额: $%.2f\n", - "en": " Total: $%.2f\n", - }, - "balance_available": { - "zh": " 可用: $%.2f\n", - "en": " Available: $%.2f\n", - }, - "balance_in_position": { - "zh": " 持仓占用: $%.2f\n\n", - "en": " In Position: $%.2f\n\n", - }, - "no_exchange": { - "zh": "⚠️ 还没有配置交易所。请在 config.yaml 的 exchanges 中添加交易所 API Key。", - "en": "⚠️ No exchange configured. Add exchange API keys in config.yaml.", - }, - "trade_usage": { - "zh": "❓ 用法: `/buy BTC 0.01` 或 `/sell ETH 0.5 3x`", - "en": "❓ Usage: `/buy BTC 0.01` or `/sell ETH 0.5 3x`", - }, - "invalid_quantity": { - "zh": "❓ 无效数量: %s", - "en": "❓ Invalid quantity: %s", - }, - "specify_quantity": { - "zh": "❓ 请指定数量: `/buy BTC 0.01`", - "en": "❓ Please specify quantity: `/buy BTC 0.01`", - }, - "confirm_trade": { - "zh": "⚡ *确认交易*\n\n• 操作: %s\n• 交易对: %s\n• 数量: %.6f\n• 杠杆: %dx\n• 交易所: %s\n\n回复 '确认' 执行交易。", - "en": "⚡ *Confirm Trade*\n\n• Action: %s\n• Symbol: %s\n• Quantity: %.6f\n• Leverage: %dx\n• Exchange: %s\n\nReply 'yes' to execute.", - }, - "trade_executed": { - "zh": "✅ *交易已执行!*\n\n• %s %s\n• 数量: %.6f\n• 杠杆: %dx\n• 交易所: %s\n• 结果: %v", - "en": "✅ *Trade Executed!*\n\n• %s %s\n• Qty: %.6f\n• Leverage: %dx\n• Exchange: %s\n• Result: %v", - }, - "no_pending": { - "zh": "没有待确认的交易", - "en": "no pending trade", - }, - "analysis_signal": { - "zh": "🔍 *%s/USDT 分析*\n\n信号: %s\n置信度: %.0f%%\n\n%s", - "en": "🔍 *%s/USDT Analysis*\n\nSignal: %s\nConfidence: %.0f%%\n\n%s", - }, - "stop_loss": { - "zh": "\n\n🛑 止损: $%.2f", - "en": "\n\n🛑 Stop Loss: $%.2f", - }, - "take_profit": { - "zh": "\n🎯 止盈: $%.2f", - "en": "\n🎯 Take Profit: $%.2f", - }, - "settings": { - "zh": "⚙️ *设置*\n\n• 语言: %s\n• 模型: %s\n• 提供商: %s\n• 交易所: %d 个已配置", - "en": "⚙️ *Settings*\n\n• Language: %s\n• Model: %s\n• Provider: %s\n• Exchanges: %d configured", - }, - "status_title": { - "zh": "📊 *NOFXi 状态*\n\n• Agent: %s\n• 模型: %s\n• 提供商: %s\n• 记忆: ✅ 在线\n• 执行: %s\n• 监控: %d 个交易对\n• 时间: %s", - "en": "📊 *NOFXi Status*\n\n• Agent: %s\n• Model: %s\n• Provider: %s\n• Memory: ✅ Online\n• Execution: %s\n• Watching: %d symbols\n• Time: %s", - }, - "bridge_connected": { - "zh": "✅ 已连接", - "en": "✅ Connected", - }, - "bridge_disconnected": { - "zh": "❌ 未连接", - "en": "❌ Not connected", - }, - "ai_timeout": { - "zh": "⏱️ AI 响应超时,请稍后再试。", - "en": "⏱️ AI response timed out, please try again.", - }, - "system_prompt": { - "zh": `你是 NOFXi,一个基于 NOFX 构建的 AI 交易 Agent。 - -你的能力: -- 市场分析和交易建议 -- 实时持仓和余额监控 -- 交易执行(开仓/平仓) -- 价格提醒和行情监控 -- 风险管理建议 - -支持多家交易所:Binance、OKX、Bybit、Bitget、KuCoin、Gate、Hyperliquid 等。 - -简洁、自信、专注交易。使用交易相关的 emoji。 -用中文回复。 -当前时间: %s`, - "en": `You are NOFXi, an AI trading agent built on NOFX. - -Your capabilities: -- Market analysis and trading recommendations -- Real-time position and balance monitoring -- Trade execution (open/close positions) -- Price alerts and market monitoring -- Risk management advice - -Supports multiple exchanges: Binance, OKX, Bybit, Bitget, KuCoin, Gate, Hyperliquid, etc. - -Be concise, confident, and action-oriented. Use trading emojis. -Respond in English. -Current time: %s`, - }, -} - -// msg returns the localized message for the given key and language. -func msg(lang, key string) string { - if m, ok := messages[key]; ok { - if s, ok := m[lang]; ok { - return s - } - if s, ok := m["en"]; ok { - return s - } - } - return key -} diff --git a/nofxi/internal/agent/router.go b/nofxi/internal/agent/router.go deleted file mode 100644 index 0336cc5d..00000000 --- a/nofxi/internal/agent/router.go +++ /dev/null @@ -1,156 +0,0 @@ -package agent - -import ( - "regexp" - "strings" -) - -// Intent represents the parsed user intent. -type Intent struct { - Type IntentType - Params map[string]string - Raw string -} - -// IntentType identifies what the user wants to do. -type IntentType int - -const ( - IntentChat IntentType = iota // General conversation - IntentTrade // Open/close a trade - IntentQuery // Query positions, balance, P/L - IntentAnalyze // Ask for market analysis - IntentSettings // Change preferences - IntentHelp // Help / command list - IntentStatus // Check agent/system status - IntentWatch // Watch symbols, price alerts - IntentStrategy // Start/stop/list strategies -) - -var ( - tradePatterns = []*regexp.Regexp{ - regexp.MustCompile(`(?i)(buy|sell|long|short|open|close)\s+(.+)`), - regexp.MustCompile(`(?i)(做多|做空|买入|卖出|开仓|平仓)\s*(.+)`), - } - queryPatterns = []*regexp.Regexp{ - regexp.MustCompile(`(?i)(position|balance|pnl|profit|loss|持仓|余额|盈亏)`), - regexp.MustCompile(`(?i)(show|list|查看)\s*(position|trade|order|持仓|订单)`), - } - analyzePatterns = []*regexp.Regexp{ - regexp.MustCompile(`(?i)(analyze|analysis|分析|看看)\s+(.+)`), - regexp.MustCompile(`(?i)(what.*think|怎么看|你觉得)\s*(.+)?`), - } - settingsPatterns = []*regexp.Regexp{ - regexp.MustCompile(`(?i)(set|config|设置|配置)\s+(.+)`), - } -) - -// Route parses user input and determines intent. -func Route(text string) Intent { - text = strings.TrimSpace(text) - lower := strings.ToLower(text) - - // Commands - if strings.HasPrefix(lower, "/") { - return routeCommand(text) - } - - // Trade intent - for _, p := range tradePatterns { - if m := p.FindStringSubmatch(text); m != nil { - return Intent{ - Type: IntentTrade, - Params: map[string]string{ - "action": strings.ToLower(m[1]), - "detail": m[2], - }, - Raw: text, - } - } - } - - // Query intent - for _, p := range queryPatterns { - if p.MatchString(text) { - return Intent{Type: IntentQuery, Raw: text} - } - } - - // Analyze intent - for _, p := range analyzePatterns { - if m := p.FindStringSubmatch(text); m != nil { - detail := "" - if len(m) > 2 { - detail = m[2] - } - return Intent{ - Type: IntentAnalyze, - Params: map[string]string{ - "detail": detail, - }, - Raw: text, - } - } - } - - // Settings intent - for _, p := range settingsPatterns { - if m := p.FindStringSubmatch(text); m != nil { - return Intent{ - Type: IntentSettings, - Params: map[string]string{ - "detail": m[2], - }, - Raw: text, - } - } - } - - // Default: chat - return Intent{Type: IntentChat, Raw: text} -} - -func routeCommand(text string) Intent { - cmd := strings.ToLower(strings.Fields(text)[0]) - switch cmd { - case "/start", "/help": - return Intent{Type: IntentHelp, Raw: text} - case "/status": - return Intent{Type: IntentStatus, Raw: text} - case "/buy", "/sell", "/long", "/short", "/open", "/close": - parts := strings.SplitN(text, " ", 2) - detail := "" - if len(parts) > 1 { - detail = parts[1] - } - return Intent{ - Type: IntentTrade, - Params: map[string]string{ - "action": strings.TrimPrefix(cmd, "/"), - "detail": detail, - }, - Raw: text, - } - case "/positions", "/balance", "/pnl": - return Intent{Type: IntentQuery, Raw: text} - case "/watch", "/unwatch", "/alert", "/price": - return Intent{Type: IntentWatch, Raw: text} - case "/strategy": - return Intent{Type: IntentStrategy, Raw: text} - case "/analyze": - parts := strings.SplitN(text, " ", 2) - detail := "" - if len(parts) > 1 { - detail = parts[1] - } - return Intent{ - Type: IntentAnalyze, - Params: map[string]string{"detail": detail}, - Raw: text, - } - case "/settings", "/config": - return Intent{Type: IntentSettings, Raw: text} - default: - return Intent{Type: IntentChat, Raw: text} - } -} diff --git a/nofxi/internal/agent/scheduler.go b/nofxi/internal/agent/scheduler.go deleted file mode 100644 index 5a479c0f..00000000 --- a/nofxi/internal/agent/scheduler.go +++ /dev/null @@ -1,187 +0,0 @@ -package agent - -import ( - "context" - "fmt" - "log/slog" - "strings" - "time" - - "nofx/nofxi/internal/memory" -) - -// Scheduler handles periodic tasks: daily reports, portfolio checks, etc. -type Scheduler struct { - agent *Agent - logger *slog.Logger - stopCh chan struct{} - notifyFn func(userID int64, text string) error -} - -// NewScheduler creates a new task scheduler. -func NewScheduler(agent *Agent, logger *slog.Logger) *Scheduler { - return &Scheduler{ - agent: agent, - logger: logger, - stopCh: make(chan struct{}), - } -} - -// SetNotifyFunc sets the notification callback. -func (s *Scheduler) SetNotifyFunc(fn func(userID int64, text string) error) { - s.notifyFn = fn -} - -// Start begins the scheduler loop. -func (s *Scheduler) Start(ctx context.Context) { - go s.loop(ctx) -} - -// Stop stops the scheduler. -func (s *Scheduler) Stop() { - close(s.stopCh) -} - -func (s *Scheduler) loop(ctx context.Context) { - // Check every minute for scheduled tasks - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - lastDailyReport := time.Time{} - lastPortfolioCheck := time.Time{} - - for { - select { - case <-ctx.Done(): - return - case <-s.stopCh: - return - case now := <-ticker.C: - hour := now.Hour() - - // Daily report at 21:00 (9 PM) - if hour == 21 && now.Sub(lastDailyReport) > 12*time.Hour { - s.sendDailyReport(ctx) - lastDailyReport = now - } - - // Portfolio check every 4 hours - if now.Sub(lastPortfolioCheck) > 4*time.Hour { - s.checkPortfolio(ctx) - lastPortfolioCheck = now - } - } - } -} - -func (s *Scheduler) sendDailyReport(ctx context.Context) { - s.logger.Info("generating daily report") - - trades, err := s.agent.memory.GetRecentTrades(50) - if err != nil { - s.logger.Error("get trades for daily report", "error", err) - return - } - - // Filter today's trades - today := time.Now().Truncate(24 * time.Hour) - var todayTrades []memory.TradeRecord - totalPnL := 0.0 - wins := 0 - - for _, t := range trades { - if t.CreatedAt.After(today) { - todayTrades = append(todayTrades, t) - totalPnL += t.PnL - if t.PnL > 0 { - wins++ - } - } - } - - if len(todayTrades) == 0 { - s.logger.Info("no trades today, skipping daily report") - return - } - - winRate := 0.0 - if len(todayTrades) > 0 { - winRate = float64(wins) / float64(len(todayTrades)) * 100 - } - - emoji := "📈" - if totalPnL < 0 { - emoji = "📉" - } - - report := fmt.Sprintf(`%s *NOFXi Daily Report — %s* - -📊 *Summary* -• Trades: %d -• Win Rate: %.1f%% -• Total P/L: $%.2f - -`, emoji, time.Now().Format("2006-01-02"), len(todayTrades), winRate, totalPnL) - - // Add trade details - if len(todayTrades) > 0 { - report += "📋 *Trades*\n" - for _, t := range todayTrades { - e := "🟢" - if t.PnL < 0 { - e = "🔴" - } - report += fmt.Sprintf("%s %s %s — P/L: $%.2f\n", e, t.Side, t.Symbol, t.PnL) - } - } - - report += "\n_Generated by NOFXi 🤖_" - - s.notify(report) -} - -func (s *Scheduler) checkPortfolio(ctx context.Context) { - if s.agent.bridge == nil { - return - } - - s.logger.Info("checking portfolio") - - var alerts []string - - for _, ex := range s.agent.config.Exchanges { - positions, err := s.agent.bridge.GetPositions(ex.Name) - if err != nil { - continue - } - - for _, p := range positions { - // Alert on large unrealized losses (> 5%) - if p.EntryPrice > 0 && p.PnL < 0 { - lossPct := (p.PnL / (p.EntryPrice * p.Size)) * 100 - if lossPct < -5 { - alerts = append(alerts, fmt.Sprintf( - "⚠️ *%s* %s: %.1f%% loss ($%.2f)", - p.Symbol, strings.ToUpper(p.Side), lossPct, p.PnL)) - } - } - } - } - - if len(alerts) > 0 { - msg := "🚨 *Portfolio Alert*\n\n" + strings.Join(alerts, "\n") - s.notify(msg) - } -} - -func (s *Scheduler) notify(text string) { - if s.notifyFn == nil { - s.logger.Warn("no notification function set, cannot send scheduled message") - return - } - for _, uid := range s.agent.config.Telegram.AllowedIDs { - if err := s.notifyFn(uid, text); err != nil { - s.logger.Error("send notification", "user_id", uid, "error", err) - } - } -} diff --git a/nofxi/internal/agent/strategy.go b/nofxi/internal/agent/strategy.go deleted file mode 100644 index ec1294df..00000000 --- a/nofxi/internal/agent/strategy.go +++ /dev/null @@ -1,199 +0,0 @@ -package agent - -import ( - "context" - "fmt" - "log/slog" - "strings" - "time" -) - -// StrategyRunner manages automated trading strategies. -type StrategyRunner struct { - agent *Agent - logger *slog.Logger - stopCh chan struct{} - - // Active strategies - activeStrategies map[string]*RunningStrategy -} - -// RunningStrategy represents an active automated strategy. -type RunningStrategy struct { - ID string - Name string - Symbol string - Interval time.Duration - Exchange string - StopCh chan struct{} - Running bool -} - -// NewStrategyRunner creates a new strategy runner. -func NewStrategyRunner(agent *Agent, logger *slog.Logger) *StrategyRunner { - return &StrategyRunner{ - agent: agent, - logger: logger, - stopCh: make(chan struct{}), - activeStrategies: make(map[string]*RunningStrategy), - } -} - -// StartStrategy begins an AI-driven trading strategy. -// The AI will periodically analyze the market and suggest/execute trades. -func (r *StrategyRunner) StartStrategy(name, symbol, exchange string, interval time.Duration) (string, error) { - id := fmt.Sprintf("%s-%s-%d", strings.ToLower(symbol), strings.ToLower(exchange), time.Now().Unix()) - - if _, exists := r.activeStrategies[id]; exists { - return "", fmt.Errorf("strategy already running: %s", id) - } - - strategy := &RunningStrategy{ - ID: id, - Name: name, - Symbol: symbol, - Interval: interval, - Exchange: exchange, - StopCh: make(chan struct{}), - Running: true, - } - - r.activeStrategies[id] = strategy - - go r.runStrategy(strategy) - - r.logger.Info("strategy started", - "id", id, - "symbol", symbol, - "exchange", exchange, - "interval", interval, - ) - - return id, nil -} - -// StopStrategy stops a running strategy. -func (r *StrategyRunner) StopStrategy(id string) error { - s, ok := r.activeStrategies[id] - if !ok { - return fmt.Errorf("strategy not found: %s", id) - } - close(s.StopCh) - s.Running = false - delete(r.activeStrategies, id) - r.logger.Info("strategy stopped", "id", id) - return nil -} - -// StopAll stops all running strategies. -func (r *StrategyRunner) StopAll() { - for id, s := range r.activeStrategies { - close(s.StopCh) - s.Running = false - r.logger.Info("strategy stopped", "id", id) - } - r.activeStrategies = make(map[string]*RunningStrategy) -} - -// ListStrategies returns all active strategies. -func (r *StrategyRunner) ListStrategies() []*RunningStrategy { - result := make([]*RunningStrategy, 0, len(r.activeStrategies)) - for _, s := range r.activeStrategies { - result = append(result, s) - } - return result -} - -func (r *StrategyRunner) runStrategy(s *RunningStrategy) { - ticker := time.NewTicker(s.Interval) - defer ticker.Stop() - - // Initial analysis - r.executeStrategyTick(s) - - for { - select { - case <-s.StopCh: - return - case <-r.stopCh: - return - case <-ticker.C: - r.executeStrategyTick(s) - } - } -} - -func (r *StrategyRunner) executeStrategyTick(s *RunningStrategy) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - r.logger.Info("strategy tick", "id", s.ID, "symbol", s.Symbol) - - // Get AI analysis - prompt := fmt.Sprintf( - "You are running an automated trading strategy for %s on %s.\n"+ - "Analyze the current market and decide: should we BUY, SELL, or HOLD?\n"+ - "Consider risk management. Only trade on high confidence signals.\n"+ - "Respond with a brief analysis and your recommendation.", - s.Symbol, s.Exchange, - ) - - analysis, err := r.agent.thinker.Analyze(ctx, prompt) - if err != nil { - r.logger.Error("strategy analysis failed", "id", s.ID, "error", err) - return - } - - r.logger.Info("strategy analysis", - "id", s.ID, - "action", analysis.Action, - "confidence", analysis.Confidence, - ) - - // Only execute on high confidence - if analysis.Confidence < 0.75 { - r.logger.Info("strategy: confidence too low, holding", "id", s.ID) - return - } - - // Notify user about the signal - if r.agent.NotifyFunc != nil { - msg := fmt.Sprintf("🤖 *Strategy Signal: %s*\n\n"+ - "Symbol: %s\n"+ - "Action: %s\n"+ - "Confidence: %.0f%%\n\n"+ - "%s", - s.Name, s.Symbol, - strings.ToUpper(analysis.Action), - analysis.Confidence*100, - analysis.Reasoning, - ) - for _, uid := range r.agent.config.Telegram.AllowedIDs { - r.agent.NotifyFunc(uid, msg) - } - } - - // TODO: Auto-execute trades based on strategy config - // For now, just notify. Users can enable auto-execution in Phase 4. -} - -// FormatStrategyList formats active strategies for display. -func (r *StrategyRunner) FormatStrategyList() string { - strategies := r.ListStrategies() - if len(strategies) == 0 { - return "📭 No active strategies.\n\nUse `/strategy start BTC 1h` to start one." - } - - var sb strings.Builder - sb.WriteString("🤖 *Active Strategies*\n\n") - for _, s := range strategies { - status := "🟢" - if !s.Running { - status = "🔴" - } - sb.WriteString(fmt.Sprintf("%s *%s* — %s on %s (every %s)\n ID: `%s`\n\n", - status, s.Name, s.Symbol, s.Exchange, s.Interval, s.ID)) - } - sb.WriteString("Stop with: `/strategy stop `") - return sb.String() -} diff --git a/nofxi/internal/agent/watchcmd.go b/nofxi/internal/agent/watchcmd.go deleted file mode 100644 index d6a6c453..00000000 --- a/nofxi/internal/agent/watchcmd.go +++ /dev/null @@ -1,125 +0,0 @@ -package agent - -import ( - "fmt" - "strconv" - "strings" -) - -// HandleWatchCommand processes /watch and /alert commands. -// Returns response text. Called from HandleMessage when intent is detected. -func (a *Agent) HandleWatchCommand(text string) string { - lower := strings.ToLower(strings.TrimSpace(text)) - - // /watch BTC - if strings.HasPrefix(lower, "/watch") { - parts := strings.Fields(text) - if len(parts) < 2 { - return a.listWatched() - } - symbol := strings.ToUpper(parts[1]) - if !strings.HasSuffix(symbol, "USDT") { - symbol += "USDT" - } - if a.monitor != nil { - a.monitor.Watch(symbol) - return fmt.Sprintf("👁️ Now watching *%s*. I'll track the price.", symbol) - } - return "⚠️ Market monitor not available." - } - - // /unwatch BTC - if strings.HasPrefix(lower, "/unwatch") { - parts := strings.Fields(text) - if len(parts) < 2 { - return "Usage: `/unwatch BTC`" - } - symbol := strings.ToUpper(parts[1]) - if !strings.HasSuffix(symbol, "USDT") { - symbol += "USDT" - } - if a.monitor != nil { - a.monitor.Unwatch(symbol) - return fmt.Sprintf("🚫 Stopped watching *%s*.", symbol) - } - return "⚠️ Market monitor not available." - } - - // /alert BTC above 100000 - if strings.HasPrefix(lower, "/alert") { - parts := strings.Fields(text) - if len(parts) < 4 { - return "Usage: `/alert BTC above 100000` or `/alert ETH below 3000`" - } - symbol := strings.ToUpper(parts[1]) - if !strings.HasSuffix(symbol, "USDT") { - symbol += "USDT" - } - direction := strings.ToLower(parts[2]) - if direction != "above" && direction != "below" { - return "Direction must be `above` or `below`." - } - threshold, err := strconv.ParseFloat(parts[3], 64) - if err != nil { - return fmt.Sprintf("Invalid price: %s", parts[3]) - } - - if a.monitor != nil { - a.monitor.Watch(symbol) // Ensure we're watching it - a.monitor.AddAlert(symbol, direction, threshold) - emoji := "📈" - if direction == "below" { - emoji = "📉" - } - return fmt.Sprintf("%s Alert set: *%s* %s $%.2f\nI'll notify you when it triggers.", - emoji, symbol, direction, threshold) - } - return "⚠️ Market monitor not available." - } - - // /price BTC - if strings.HasPrefix(lower, "/price") { - parts := strings.Fields(text) - if len(parts) < 2 { - return "Usage: `/price BTC`" - } - symbol := strings.ToUpper(parts[1]) - if !strings.HasSuffix(symbol, "USDT") { - symbol += "USDT" - } - if a.monitor != nil { - if snap, ok := a.monitor.GetSnapshot(symbol); ok && snap.LastPrice > 0 { - return fmt.Sprintf("💰 *%s*: $%.4f\n_Updated: %s_", - symbol, snap.LastPrice, snap.UpdatedAt.Format("15:04:05")) - } - // Not watching yet, watch and return message - a.monitor.Watch(symbol) - return fmt.Sprintf("👁️ Started watching *%s*. Price will be available in ~30s.", symbol) - } - return "⚠️ Market monitor not available." - } - - return "" -} - -func (a *Agent) listWatched() string { - if a.monitor == nil { - return "⚠️ Market monitor not available." - } - snaps := a.monitor.GetAllSnapshots() - if len(snaps) == 0 { - return "📭 Not watching any symbols. Use `/watch BTC` to start." - } - - var sb strings.Builder - sb.WriteString("👁️ *Watching*\n\n") - for symbol, snap := range snaps { - if snap.LastPrice > 0 { - sb.WriteString(fmt.Sprintf("• *%s*: $%.4f (%s)\n", - symbol, snap.LastPrice, snap.UpdatedAt.Format("15:04:05"))) - } else { - sb.WriteString(fmt.Sprintf("• *%s*: waiting for data...\n", symbol)) - } - } - return sb.String() -} diff --git a/nofxi/internal/execution/bridge.go b/nofxi/internal/execution/bridge.go deleted file mode 100644 index 81818e34..00000000 --- a/nofxi/internal/execution/bridge.go +++ /dev/null @@ -1,192 +0,0 @@ -package execution - -import ( - "fmt" - "log/slog" - "strings" - "sync" -) - -// TraderFactory creates a NOFX Trader by exchange name. -// Injected from main.go where nofx exchange packages are available. -type TraderFactory func(exchange, apiKey, apiSecret, passphrase string, testnet bool) (NofxTrader, error) - -// NofxTrader mirrors nofx/trader/types.Trader interface. -// We redefine it here to avoid a direct import cycle with the parent module. -type NofxTrader interface { - GetBalance() (map[string]interface{}, error) - GetPositions() ([]map[string]interface{}, error) - 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) - SetLeverage(symbol string, leverage int) error - GetMarketPrice(symbol string) (float64, error) - SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error - SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error - CancelAllOrders(symbol string) error -} - -// Bridge connects NOFXi to NOFX trading engine. -type Bridge struct { - mu sync.RWMutex - traders map[string]NofxTrader // exchange name → trader - factory TraderFactory - logger *slog.Logger -} - -// NewBridge creates a new execution bridge. -func NewBridge(factory TraderFactory, logger *slog.Logger) *Bridge { - return &Bridge{ - traders: make(map[string]NofxTrader), - factory: factory, - logger: logger, - } -} - -// RegisterTrader registers a pre-configured trader for an exchange. -func (b *Bridge) RegisterTrader(exchange string, trader NofxTrader) { - b.mu.Lock() - defer b.mu.Unlock() - b.traders[strings.ToLower(exchange)] = trader - b.logger.Info("trader registered", "exchange", exchange) -} - -// getTrader returns the trader for the given exchange. -func (b *Bridge) getTrader(exchange string) (NofxTrader, error) { - b.mu.RLock() - defer b.mu.RUnlock() - t, ok := b.traders[strings.ToLower(exchange)] - if !ok { - return nil, fmt.Errorf("exchange %q not configured", exchange) - } - return t, nil -} - -// PlaceOrder executes a trade via the NOFX trader. -func (b *Bridge) PlaceOrder(exchange, symbol, side string, quantity float64, leverage int) (map[string]interface{}, error) { - trader, err := b.getTrader(exchange) - if err != nil { - return nil, err - } - - // Set leverage first - if leverage > 0 { - if err := trader.SetLeverage(symbol, leverage); err != nil { - b.logger.Warn("set leverage failed", "error", err) - } - } - - side = strings.ToUpper(side) - switch side { - case "BUY", "LONG", "OPEN_LONG": - b.logger.Info("opening long", "exchange", exchange, "symbol", symbol, "qty", quantity, "leverage", leverage) - return trader.OpenLong(symbol, quantity, leverage) - case "SELL", "SHORT", "OPEN_SHORT": - b.logger.Info("opening short", "exchange", exchange, "symbol", symbol, "qty", quantity, "leverage", leverage) - return trader.OpenShort(symbol, quantity, leverage) - case "CLOSE_LONG": - b.logger.Info("closing long", "exchange", exchange, "symbol", symbol, "qty", quantity) - return trader.CloseLong(symbol, quantity) - case "CLOSE_SHORT": - b.logger.Info("closing short", "exchange", exchange, "symbol", symbol, "qty", quantity) - return trader.CloseShort(symbol, quantity) - default: - return nil, fmt.Errorf("unknown side: %s", side) - } -} - -// GetPositions returns all open positions from an exchange. -func (b *Bridge) GetPositions(exchange string) ([]Position, error) { - trader, err := b.getTrader(exchange) - if err != nil { - return nil, err - } - - raw, err := trader.GetPositions() - if err != nil { - return nil, fmt.Errorf("get positions: %w", err) - } - - var positions []Position - for _, p := range raw { - pos := Position{ - Exchange: exchange, - Symbol: fmt.Sprint(p["symbol"]), - Side: fmt.Sprint(p["side"]), - } - if v, ok := p["size"].(float64); ok { - pos.Size = v - } - if v, ok := p["entryPrice"].(float64); ok { - pos.EntryPrice = v - } - if v, ok := p["markPrice"].(float64); ok { - pos.MarkPrice = v - } - if v, ok := p["unrealizedPnl"].(float64); ok { - pos.PnL = v - } - if v, ok := p["leverage"].(float64); ok { - pos.Leverage = v - } - // Skip empty positions - if pos.Size != 0 { - positions = append(positions, pos) - } - } - return positions, nil -} - -// GetBalance returns account balance from an exchange. -func (b *Bridge) GetBalance(exchange string) (*Balance, error) { - trader, err := b.getTrader(exchange) - if err != nil { - return nil, err - } - - raw, err := trader.GetBalance() - if err != nil { - return nil, fmt.Errorf("get balance: %w", err) - } - - bal := &Balance{ - Exchange: exchange, - Currency: "USDT", - } - if v, ok := raw["totalBalance"].(float64); ok { - bal.Total = v - } - if v, ok := raw["availableBalance"].(float64); ok { - bal.Available = v - } - bal.InPosition = bal.Total - bal.Available - return bal, nil -} - -// GetPrice returns the current market price for a symbol. -func (b *Bridge) GetPrice(exchange, symbol string) (float64, error) { - trader, err := b.getTrader(exchange) - if err != nil { - return 0, err - } - return trader.GetMarketPrice(symbol) -} - -// SetStopLoss sets a stop-loss order. -func (b *Bridge) SetStopLoss(exchange, symbol, positionSide string, quantity, price float64) error { - trader, err := b.getTrader(exchange) - if err != nil { - return err - } - return trader.SetStopLoss(symbol, positionSide, quantity, price) -} - -// SetTakeProfit sets a take-profit order. -func (b *Bridge) SetTakeProfit(exchange, symbol, positionSide string, quantity, price float64) error { - trader, err := b.getTrader(exchange) - if err != nil { - return err - } - return trader.SetTakeProfit(symbol, positionSide, quantity, price) -} diff --git a/nofxi/internal/execution/execution.go b/nofxi/internal/execution/execution.go deleted file mode 100644 index 192af2ca..00000000 --- a/nofxi/internal/execution/execution.go +++ /dev/null @@ -1,47 +0,0 @@ -// Package execution implements the Execution Layer. -// -// Phase 2 — stubs for now. -// -// Planned capabilities: -// - Multi-exchange order execution (via NOFX engine) -// - Position management (open/close/adjust) -// - Stop-loss and take-profit management -// - x402 payment integration via Claw402 -// - Safe mode and risk controls -package execution - -// Executor is the trade execution interface. -type Executor interface { - // PlaceOrder places a trade order on the exchange. - PlaceOrder(exchange, symbol, side string, price, quantity float64) (string, error) - - // ClosePosition closes an existing position. - ClosePosition(exchange, symbol string) error - - // GetPositions returns current open positions. - GetPositions(exchange string) ([]Position, error) - - // GetBalance returns account balance. - GetBalance(exchange string) (*Balance, error) -} - -// Position represents an open position. -type Position struct { - Exchange string `json:"exchange"` - Symbol string `json:"symbol"` - Side string `json:"side"` - Size float64 `json:"size"` - EntryPrice float64 `json:"entry_price"` - MarkPrice float64 `json:"mark_price"` - PnL float64 `json:"pnl"` - Leverage float64 `json:"leverage"` -} - -// Balance represents account balance. -type Balance struct { - Exchange string `json:"exchange"` - Total float64 `json:"total"` - Available float64 `json:"available"` - InPosition float64 `json:"in_position"` - Currency string `json:"currency"` -} diff --git a/nofxi/internal/execution/factory.go b/nofxi/internal/execution/factory.go deleted file mode 100644 index 7fe35ec3..00000000 --- a/nofxi/internal/execution/factory.go +++ /dev/null @@ -1,52 +0,0 @@ -package execution - -import ( - "fmt" - "strings" - - "nofx/trader/binance" - "nofx/trader/bitget" - "nofx/trader/bybit" - "nofx/trader/gate" - "nofx/trader/kucoin" - "nofx/trader/okx" -) - -// ExchangeConfig holds credentials for creating a trader. -type ExchangeConfig struct { - Name string - APIKey string - APISecret string - Passphrase string - Testnet bool -} - -// CreateTrader creates a NofxTrader for the given exchange. -func CreateTrader(cfg ExchangeConfig) (NofxTrader, error) { - switch strings.ToLower(cfg.Name) { - case "binance": - return binance.NewFuturesTrader(cfg.APIKey, cfg.APISecret, "nofxi"), nil - - case "okx": - return okx.NewOKXTrader(cfg.APIKey, cfg.APISecret, cfg.Passphrase), nil - - case "bybit": - return bybit.NewBybitTrader(cfg.APIKey, cfg.APISecret), nil - - case "bitget": - return bitget.NewBitgetTrader(cfg.APIKey, cfg.APISecret, cfg.Passphrase), nil - - case "kucoin": - return kucoin.NewKuCoinTrader(cfg.APIKey, cfg.APISecret, cfg.Passphrase), nil - - case "gate": - return gate.NewGateTrader(cfg.APIKey, cfg.APISecret), nil - - // Hyperliquid needs private key, not API key/secret - // case "hyperliquid": - // return hyperliquid.NewHyperliquidTrader(cfg.APIKey, "", cfg.Testnet) - - default: - return nil, fmt.Errorf("unsupported exchange: %s", cfg.Name) - } -} diff --git a/nofxi/internal/interaction/formatter.go b/nofxi/internal/interaction/formatter.go deleted file mode 100644 index 0cab57fc..00000000 --- a/nofxi/internal/interaction/formatter.go +++ /dev/null @@ -1,68 +0,0 @@ -package interaction - -import ( - "fmt" - "strings" - "time" -) - -// FormatTradeAlert formats a trade event for Telegram notification. -func FormatTradeAlert(action, symbol, exchange string, price, quantity float64) string { - emoji := "🟢" - if action == "sell" || action == "short" || action == "close" { - emoji = "🔴" - } - return fmt.Sprintf(`%s *Trade Executed* - -• Action: %s -• Symbol: %s -• Exchange: %s -• Price: $%.4f -• Quantity: %.6f -• Value: $%.2f -• Time: %s`, - emoji, - strings.ToUpper(action), - symbol, - exchange, - price, - quantity, - price*quantity, - time.Now().Format("15:04:05"), - ) -} - -// FormatDailyReport formats a daily P/L summary. -func FormatDailyReport(totalPnL float64, trades int, winRate float64) string { - emoji := "📈" - if totalPnL < 0 { - emoji = "📉" - } - return fmt.Sprintf(`%s *Daily Report — %s* - -• Trades: %d -• Win Rate: %.1f%% -• P/L: $%.2f - -Keep going! 💪`, - emoji, - time.Now().Format("2006-01-02"), - trades, - winRate, - totalPnL, - ) -} - -// FormatPriceAlert formats a price alert notification. -func FormatPriceAlert(symbol string, price float64, direction string, threshold float64) string { - emoji := "🚨" - if direction == "above" { - emoji = "📈" - } else { - emoji = "📉" - } - return fmt.Sprintf(`%s *Price Alert* - -%s hit $%.4f (%s $%.4f threshold)`, - emoji, symbol, price, direction, threshold) -} diff --git a/nofxi/internal/interaction/interaction.go b/nofxi/internal/interaction/interaction.go deleted file mode 100644 index ae051bd8..00000000 --- a/nofxi/internal/interaction/interaction.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package interaction implements the Interaction Layer. -// -// Primary interface: Telegram bot -// - Natural language understanding for trading commands -// - Access control (allowed user IDs) -// - Typing indicators and markdown formatting -// - Proactive notifications -// -// Future: Web UI API endpoints -package interaction diff --git a/nofxi/internal/interaction/kline.go b/nofxi/internal/interaction/kline.go deleted file mode 100644 index c0dcc5d7..00000000 --- a/nofxi/internal/interaction/kline.go +++ /dev/null @@ -1,126 +0,0 @@ -package interaction - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - "time" -) - -// KlineBar represents a single candlestick. -type KlineBar struct { - Time int64 `json:"time"` - Open float64 `json:"open"` - High float64 `json:"high"` - Low float64 `json:"low"` - Close float64 `json:"close"` - Volume float64 `json:"volume"` -} - -// fetchKlines gets kline data from Binance public API (no auth needed). -func fetchKlines(symbol, interval string, limit int) ([]KlineBar, error) { - if limit <= 0 { - limit = 200 - } - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=%d", - symbol, interval, limit) - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(url) - if err != nil { - return nil, fmt.Errorf("fetch klines: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var raw [][]interface{} - if err := json.Unmarshal(body, &raw); err != nil { - return nil, fmt.Errorf("parse klines: %w", err) - } - - bars := make([]KlineBar, 0, len(raw)) - for _, k := range raw { - if len(k) < 6 { - continue - } - bar := KlineBar{ - Time: int64(k[0].(float64)) / 1000, // ms → seconds for lightweight-charts - } - bar.Open, _ = strconv.ParseFloat(k[1].(string), 64) - bar.High, _ = strconv.ParseFloat(k[2].(string), 64) - bar.Low, _ = strconv.ParseFloat(k[3].(string), 64) - bar.Close, _ = strconv.ParseFloat(k[4].(string), 64) - bar.Volume, _ = strconv.ParseFloat(k[5].(string), 64) - bars = append(bars, bar) - } - return bars, nil -} - -// RegisterKlineRoutes adds kline API endpoints to the mux. -func RegisterKlineRoutes(mux *http.ServeMux) { - // GET /api/klines?symbol=BTCUSDT&interval=1h&limit=200 - mux.HandleFunc("/api/klines", func(w http.ResponseWriter, r *http.Request) { - symbol := r.URL.Query().Get("symbol") - if symbol == "" { - symbol = "BTCUSDT" - } - interval := r.URL.Query().Get("interval") - if interval == "" { - interval = "1h" - } - limitStr := r.URL.Query().Get("limit") - limit := 200 - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { - limit = l - } - if limit > 1000 { - limit = 1000 - } - - bars, err := fetchKlines(symbol, interval, limit) - if err != nil { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadGateway) - json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - json.NewEncoder(w).Encode(bars) - }) - - // GET /api/ticker?symbol=BTCUSDT — current price + 24h change - mux.HandleFunc("/api/ticker", func(w http.ResponseWriter, r *http.Request) { - symbol := r.URL.Query().Get("symbol") - if symbol == "" { - symbol = "BTCUSDT" - } - - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol) - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(url) - if err != nil { - w.WriteHeader(http.StatusBadGateway) - json.NewEncoder(w).Encode(map[string]string{"error": err.Error()}) - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Write(body) - }) -} diff --git a/nofxi/internal/interaction/telegram.go b/nofxi/internal/interaction/telegram.go deleted file mode 100644 index 5063b441..00000000 --- a/nofxi/internal/interaction/telegram.go +++ /dev/null @@ -1,137 +0,0 @@ -package interaction - -import ( - "context" - "fmt" - "log/slog" - "time" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -// MessageHandler is called for each incoming message. -type MessageHandler func(ctx context.Context, userID int64, text string) (string, error) - -// TelegramBot wraps the Telegram Bot API. -type TelegramBot struct { - bot *tgbotapi.BotAPI - handler MessageHandler - allowedIDs map[int64]bool // Empty = allow all - logger *slog.Logger -} - -// NewTelegramBot creates a new Telegram bot. -func NewTelegramBot(token string, allowedIDs []int64, handler MessageHandler, logger *slog.Logger) (*TelegramBot, error) { - bot, err := tgbotapi.NewBotAPI(token) - if err != nil { - return nil, fmt.Errorf("create bot: %w", err) - } - - allowed := make(map[int64]bool) - for _, id := range allowedIDs { - allowed[id] = true - } - - logger.Info("telegram bot authorized", "username", bot.Self.UserName) - - return &TelegramBot{ - bot: bot, - handler: handler, - allowedIDs: allowed, - logger: logger, - }, nil -} - -// Start begins polling for updates. Blocks until context is cancelled. -func (t *TelegramBot) Start(ctx context.Context) error { - u := tgbotapi.NewUpdate(0) - u.Timeout = 60 - - updates := t.bot.GetUpdatesChan(u) - - t.logger.Info("telegram bot started, listening for messages...") - - for { - select { - case <-ctx.Done(): - t.bot.StopReceivingUpdates() - t.logger.Info("telegram bot stopped") - return nil - case update := <-updates: - if update.Message == nil { - continue - } - go t.handleUpdate(ctx, update) - } - } -} - -func (t *TelegramBot) handleUpdate(parentCtx context.Context, update tgbotapi.Update) { - msg := update.Message - userID := msg.From.ID - - // Access control - if len(t.allowedIDs) > 0 && !t.allowedIDs[userID] { - t.logger.Warn("unauthorized user", "user_id", userID, "username", msg.From.UserName) - t.sendText(msg.Chat.ID, "⛔ Unauthorized. Contact the admin to get access.") - return - } - - text := msg.Text - if text == "" { - return - } - - t.logger.Info("received message", - "user_id", userID, - "username", msg.From.UserName, - "text", text, - ) - - // Send "typing" indicator - typing := tgbotapi.NewChatAction(msg.Chat.ID, tgbotapi.ChatTyping) - t.bot.Send(typing) - - // Process message with timeout - ctx, cancel := context.WithTimeout(parentCtx, 55*time.Second) - defer cancel() - - response, err := t.handler(ctx, userID, text) - if err != nil { - t.logger.Error("handle message", "error", err) - if ctx.Err() != nil { - response = "⏱️ AI 响应超时,请稍后再试。" - } else { - response = fmt.Sprintf("⚠️ Error: %v", err) - } - } - - t.logger.Info("sending response", "user_id", userID, "len", len(response)) - t.sendMarkdown(msg.Chat.ID, response) -} - -func (t *TelegramBot) sendText(chatID int64, text string) { - msg := tgbotapi.NewMessage(chatID, text) - if _, err := t.bot.Send(msg); err != nil { - t.logger.Error("send message", "error", err) - } -} - -func (t *TelegramBot) sendMarkdown(chatID int64, text string) { - msg := tgbotapi.NewMessage(chatID, text) - msg.ParseMode = tgbotapi.ModeMarkdown - - if _, err := t.bot.Send(msg); err != nil { - // Retry without markdown if parsing fails - t.logger.Warn("markdown send failed, retrying plain", "error", err) - t.sendText(chatID, text) - } -} - -// SendNotification sends a proactive message to a user. -func (t *TelegramBot) SendNotification(userID int64, text string) error { - msg := tgbotapi.NewMessage(userID, text) - msg.ParseMode = tgbotapi.ModeMarkdown - _, err := t.bot.Send(msg) - return err -} diff --git a/nofxi/internal/interaction/web.go b/nofxi/internal/interaction/web.go deleted file mode 100644 index b95349dc..00000000 --- a/nofxi/internal/interaction/web.go +++ /dev/null @@ -1,195 +0,0 @@ -package interaction - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "net/http" - "os" - "path/filepath" - "strconv" - "time" -) - -// WebServer provides a REST API and Web UI for NOFXi. -type WebServer struct { - handler MessageHandler - port int - webDir string // Path to web/ directory for static files - logger *slog.Logger - server *http.Server -} - -// NewWebServer creates a new web API server. -// webDir is the path to the web/ directory containing index.html. -func NewWebServer(port int, handler MessageHandler, webDir string, logger *slog.Logger) *WebServer { - return &WebServer{ - handler: handler, - port: port, - webDir: webDir, - logger: logger, - } -} - -// chatRequest is the API request body. -type chatRequest struct { - UserID int64 `json:"user_id"` - Message string `json:"message"` - Lang string `json:"lang"` // "zh" or "en" -} - -// chatResponse is the API response body. -type chatAPIResponse struct { - Response string `json:"response"` - Error string `json:"error,omitempty"` -} - -// Start begins listening. Blocks until context is cancelled. -func (w *WebServer) Start(ctx context.Context) error { - mux := http.NewServeMux() - - // Health check - mux.HandleFunc("/health", func(rw http.ResponseWriter, r *http.Request) { - json.NewEncoder(rw).Encode(map[string]string{ - "status": "ok", - "agent": "NOFXi", - "time": time.Now().Format(time.RFC3339), - }) - }) - - // Chat endpoint - mux.HandleFunc("/api/chat", func(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(rw, "method not allowed", http.StatusMethodNotAllowed) - return - } - - var req chatRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeJSON(rw, http.StatusBadRequest, chatAPIResponse{Error: "invalid request body"}) - return - } - - if req.Message == "" { - writeJSON(rw, http.StatusBadRequest, chatAPIResponse{Error: "message is required"}) - return - } - if req.UserID == 0 { - req.UserID = 1 // Default user for API access - } - - // Pass language preference via message prefix (agent will extract it) - message := req.Message - if req.Lang != "" { - message = "[lang:" + req.Lang + "] " + message - } - resp, err := w.handler(r.Context(), req.UserID, message) - if err != nil { - writeJSON(rw, http.StatusInternalServerError, chatAPIResponse{Error: err.Error()}) - return - } - - writeJSON(rw, http.StatusOK, chatAPIResponse{Response: resp}) - }) - - // OpenAI-compatible endpoint (so other tools can talk to NOFXi) - mux.HandleFunc("/v1/chat/completions", func(rw http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(rw, "method not allowed", http.StatusMethodNotAllowed) - return - } - - var body struct { - Messages []struct { - Role string `json:"role"` - Content string `json:"content"` - } `json:"messages"` - } - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - writeJSON(rw, http.StatusBadRequest, map[string]string{"error": "invalid request"}) - return - } - - // Get the last user message - userMsg := "" - for i := len(body.Messages) - 1; i >= 0; i-- { - if body.Messages[i].Role == "user" { - userMsg = body.Messages[i].Content - break - } - } - if userMsg == "" { - writeJSON(rw, http.StatusBadRequest, map[string]string{"error": "no user message"}) - return - } - - resp, err := w.handler(r.Context(), 1, userMsg) - if err != nil { - writeJSON(rw, http.StatusInternalServerError, map[string]string{"error": err.Error()}) - return - } - - // Return OpenAI-compatible response - writeJSON(rw, http.StatusOK, map[string]interface{}{ - "id": fmt.Sprintf("nofxi-%d", time.Now().UnixNano()), - "object": "chat.completion", - "created": time.Now().Unix(), - "model": "nofxi", - "choices": []map[string]interface{}{ - { - "index": 0, - "message": map[string]string{"role": "assistant", "content": resp}, - "finish_reason": "stop", - }, - }, - }) - }) - - // Register kline/market data API - RegisterKlineRoutes(mux) - - // Serve web UI static files - if w.webDir != "" { - if _, err := os.Stat(filepath.Join(w.webDir, "index.html")); err == nil { - mux.Handle("/", http.FileServer(http.Dir(w.webDir))) - w.logger.Info("serving web UI", "dir", w.webDir) - } - } - - addr := fmt.Sprintf(":%d", w.port) - w.server = &http.Server{ - Addr: addr, - Handler: mux, - } - - w.logger.Info("web server starting", "addr", addr) - - go func() { - <-ctx.Done() - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - w.server.Shutdown(shutdownCtx) - }() - - if err := w.server.ListenAndServe(); err != http.ErrServerClosed { - return err - } - return nil -} - -// Port returns the configured port. -func (w *WebServer) Port() int { - return w.port -} - -// PortStr returns port as string. -func (w *WebServer) PortStr() string { - return strconv.Itoa(w.port) -} - -func writeJSON(w http.ResponseWriter, status int, v interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(v) -} diff --git a/nofxi/internal/memory/learner.go b/nofxi/internal/memory/learner.go deleted file mode 100644 index 5473b067..00000000 --- a/nofxi/internal/memory/learner.go +++ /dev/null @@ -1,229 +0,0 @@ -package memory - -import ( - "database/sql" - "fmt" - "time" -) - -// UserProfile captures what the AI has learned about a user's trading behavior. -type UserProfile struct { - UserID int64 `json:"user_id"` - TotalTrades int `json:"total_trades"` - WinRate float64 `json:"win_rate"` - AvgHoldTime float64 `json:"avg_hold_time_hours"` - PreferredSide string `json:"preferred_side"` // "long", "short", "balanced" - RiskTolerance string `json:"risk_tolerance"` // "conservative", "moderate", "aggressive" - FavoriteSymbols []string `json:"favorite_symbols"` - AvgLeverage float64 `json:"avg_leverage"` - BestStrategy string `json:"best_strategy"` - WorstStrategy string `json:"worst_strategy"` - TotalPnL float64 `json:"total_pnl"` - BiggestWin float64 `json:"biggest_win"` - BiggestLoss float64 `json:"biggest_loss"` - LastAnalyzed time.Time `json:"last_analyzed"` -} - -// Lesson is an insight learned from past trading. -type Lesson struct { - ID int64 `json:"id"` - UserID int64 `json:"user_id"` - Type string `json:"type"` // "win_pattern", "loss_pattern", "risk_insight", "strategy_note" - Content string `json:"content"` // Natural language description - Symbol string `json:"symbol,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -// InitLearnerTables creates the learner-specific tables. -func (s *Store) InitLearnerTables() error { - queries := []string{ - `CREATE TABLE IF NOT EXISTS user_profiles ( - user_id INTEGER PRIMARY KEY, - total_trades INTEGER DEFAULT 0, - win_rate REAL DEFAULT 0, - avg_hold_time REAL DEFAULT 0, - preferred_side TEXT DEFAULT 'balanced', - risk_tolerance TEXT DEFAULT 'moderate', - favorite_symbols TEXT DEFAULT '', - avg_leverage REAL DEFAULT 1, - best_strategy TEXT DEFAULT '', - worst_strategy TEXT DEFAULT '', - total_pnl REAL DEFAULT 0, - biggest_win REAL DEFAULT 0, - biggest_loss REAL DEFAULT 0, - last_analyzed DATETIME - )`, - `CREATE TABLE IF NOT EXISTS lessons ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - type TEXT NOT NULL, - content TEXT NOT NULL, - symbol TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - `CREATE TABLE IF NOT EXISTS ai_predictions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - symbol TEXT NOT NULL, - predicted_action TEXT NOT NULL, - predicted_confidence REAL, - actual_result TEXT, - actual_pnl REAL, - model TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - resolved_at DATETIME - )`, - `CREATE INDEX IF NOT EXISTS idx_lessons_user ON lessons(user_id, type)`, - `CREATE INDEX IF NOT EXISTS idx_predictions_symbol ON ai_predictions(symbol, created_at)`, - } - - for _, q := range queries { - if _, err := s.db.Exec(q); err != nil { - return fmt.Errorf("learner migration: %w", err) - } - } - return nil -} - -// SaveLesson stores a trading lesson. -func (s *Store) SaveLesson(userID int64, lessonType, content, symbol string) error { - _, err := s.db.Exec( - `INSERT INTO lessons (user_id, type, content, symbol) VALUES (?, ?, ?, ?)`, - userID, lessonType, content, symbol, - ) - return err -} - -// GetLessons retrieves lessons for a user. -func (s *Store) GetLessons(userID int64, limit int) ([]Lesson, error) { - rows, err := s.db.Query( - `SELECT id, user_id, type, content, COALESCE(symbol,''), created_at - FROM lessons WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`, - userID, limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - var lessons []Lesson - for rows.Next() { - var l Lesson - if err := rows.Scan(&l.ID, &l.UserID, &l.Type, &l.Content, &l.Symbol, &l.CreatedAt); err != nil { - return nil, err - } - lessons = append(lessons, l) - } - return lessons, nil -} - -// SavePrediction logs an AI prediction for later evaluation. -func (s *Store) SavePrediction(symbol, action string, confidence float64, model string) (int64, error) { - res, err := s.db.Exec( - `INSERT INTO ai_predictions (symbol, predicted_action, predicted_confidence, model) VALUES (?, ?, ?, ?)`, - symbol, action, confidence, model, - ) - if err != nil { - return 0, err - } - return res.LastInsertId() -} - -// ResolvePrediction updates a prediction with the actual result. -func (s *Store) ResolvePrediction(id int64, result string, pnl float64) error { - _, err := s.db.Exec( - `UPDATE ai_predictions SET actual_result = ?, actual_pnl = ?, resolved_at = ? WHERE id = ?`, - result, pnl, time.Now(), id, - ) - return err -} - -// GetPredictionAccuracy returns the accuracy of AI predictions. -func (s *Store) GetPredictionAccuracy(model string) (total int, correct int, avgPnL float64, err error) { - var pnl sql.NullFloat64 - err = s.db.QueryRow( - `SELECT COUNT(*), SUM(CASE WHEN actual_pnl > 0 THEN 1 ELSE 0 END), AVG(actual_pnl) - FROM ai_predictions WHERE resolved_at IS NOT NULL AND (? = '' OR model = ?)`, - model, model, - ).Scan(&total, &correct, &pnl) - if pnl.Valid { - avgPnL = pnl.Float64 - } - return -} - -// AnalyzeUserProfile builds a profile from trading history. -func (s *Store) AnalyzeUserProfile(userID int64) (*UserProfile, error) { - trades, err := s.GetRecentTrades(1000) - if err != nil { - return nil, err - } - - if len(trades) == 0 { - return &UserProfile{UserID: userID}, nil - } - - profile := &UserProfile{ - UserID: userID, - TotalTrades: len(trades), - } - - wins := 0 - symbolCount := make(map[string]int) - var totalPnL, bigWin, bigLoss, totalLev float64 - longCount, shortCount := 0, 0 - - for _, t := range trades { - totalPnL += t.PnL - if t.PnL > 0 { - wins++ - } - if t.PnL > bigWin { - bigWin = t.PnL - } - if t.PnL < bigLoss { - bigLoss = t.PnL - } - symbolCount[t.Symbol]++ - if t.Side == "long" || t.Side == "buy" { - longCount++ - } else { - shortCount++ - } - } - - profile.WinRate = float64(wins) / float64(len(trades)) * 100 - profile.TotalPnL = totalPnL - profile.BiggestWin = bigWin - profile.BiggestLoss = bigLoss - profile.AvgLeverage = totalLev / float64(len(trades)) - - if longCount > shortCount*2 { - profile.PreferredSide = "long" - } else if shortCount > longCount*2 { - profile.PreferredSide = "short" - } else { - profile.PreferredSide = "balanced" - } - - // Top symbols - var favs []string - for sym := range symbolCount { - favs = append(favs, sym) - } - if len(favs) > 5 { - favs = favs[:5] - } - profile.FavoriteSymbols = favs - - // Risk tolerance based on leverage and loss patterns - if profile.BiggestLoss < -500 || profile.AvgLeverage > 10 { - profile.RiskTolerance = "aggressive" - } else if profile.BiggestLoss < -100 || profile.AvgLeverage > 3 { - profile.RiskTolerance = "moderate" - } else { - profile.RiskTolerance = "conservative" - } - - profile.LastAnalyzed = time.Now() - return profile, nil -} diff --git a/nofxi/internal/memory/memory.go b/nofxi/internal/memory/memory.go deleted file mode 100644 index 6f8460d7..00000000 --- a/nofxi/internal/memory/memory.go +++ /dev/null @@ -1,11 +0,0 @@ -// Package memory implements the Memory Layer. -// -// Provides persistent storage for: -// - Trade history and P/L tracking -// - Conversation context (per-user message history) -// - User preferences and risk profiles -// - Strategy performance metrics -// - Market pattern snapshots -// -// Backend: SQLite (pure Go driver, zero CGO) -package memory diff --git a/nofxi/internal/memory/models.go b/nofxi/internal/memory/models.go deleted file mode 100644 index 06624c5f..00000000 --- a/nofxi/internal/memory/models.go +++ /dev/null @@ -1,64 +0,0 @@ -package memory - -import "time" - -// TradeRecord stores a completed or active trade. -type TradeRecord struct { - ID int64 `json:"id"` - Exchange string `json:"exchange"` - Symbol string `json:"symbol"` - Side string `json:"side"` // "buy" or "sell" - Type string `json:"type"` // "market", "limit" - Price float64 `json:"price"` - Quantity float64 `json:"quantity"` - PnL float64 `json:"pnl"` // Realized P/L - Fee float64 `json:"fee"` - Status string `json:"status"` // "open", "closed", "cancelled" - AIModel string `json:"ai_model"` // Which AI model made the decision - AIReason string `json:"ai_reason"` // Why AI made this decision - StrategyID string `json:"strategy_id"` - CreatedAt time.Time `json:"created_at"` - ClosedAt *time.Time `json:"closed_at,omitempty"` -} - -// UserPreference stores user settings and preferences. -type UserPreference struct { - ID int64 `json:"id"` - UserID int64 `json:"user_id"` // Telegram user ID - Key string `json:"key"` - Value string `json:"value"` - UpdatedAt time.Time `json:"updated_at"` -} - -// Conversation stores chat messages for context. -type Conversation struct { - ID int64 `json:"id"` - UserID int64 `json:"user_id"` - Role string `json:"role"` // "user", "assistant", "system" - Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` -} - -// StrategyPerformance tracks how well a strategy has performed. -type StrategyPerformance struct { - ID int64 `json:"id"` - StrategyID string `json:"strategy_id"` - StrategyName string `json:"strategy_name"` - TotalTrades int `json:"total_trades"` - WinRate float64 `json:"win_rate"` - TotalPnL float64 `json:"total_pnl"` - MaxDrawdown float64 `json:"max_drawdown"` - SharpeRatio float64 `json:"sharpe_ratio"` - UpdatedAt time.Time `json:"updated_at"` -} - -// MarketSnapshot stores a point-in-time market state for pattern recognition. -type MarketSnapshot struct { - ID int64 `json:"id"` - Symbol string `json:"symbol"` - Price float64 `json:"price"` - Volume24h float64 `json:"volume_24h"` - Change24h float64 `json:"change_24h"` - Note string `json:"note"` // AI observation about this snapshot - CreatedAt time.Time `json:"created_at"` -} diff --git a/nofxi/internal/memory/store.go b/nofxi/internal/memory/store.go deleted file mode 100644 index 80641d8f..00000000 --- a/nofxi/internal/memory/store.go +++ /dev/null @@ -1,216 +0,0 @@ -package memory - -import ( - "database/sql" - "fmt" - "time" - - _ "modernc.org/sqlite" -) - -// Store is the SQLite-backed memory store. -type Store struct { - db *sql.DB -} - -// NewStore opens (or creates) the SQLite database. -func NewStore(dbPath string) (*Store, error) { - db, err := sql.Open("sqlite", dbPath) - if err != nil { - return nil, fmt.Errorf("open db: %w", err) - } - - // Enable WAL mode for better concurrency - if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil { - return nil, fmt.Errorf("set WAL: %w", err) - } - - s := &Store{db: db} - if err := s.migrate(); err != nil { - return nil, fmt.Errorf("migrate: %w", err) - } - - return s, nil -} - -// Close closes the database. -func (s *Store) Close() error { - return s.db.Close() -} - -func (s *Store) migrate() error { - queries := []string{ - `CREATE TABLE IF NOT EXISTS trades ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - exchange TEXT NOT NULL, - symbol TEXT NOT NULL, - side TEXT NOT NULL, - type TEXT NOT NULL DEFAULT 'market', - price REAL NOT NULL, - quantity REAL NOT NULL, - pnl REAL DEFAULT 0, - fee REAL DEFAULT 0, - status TEXT NOT NULL DEFAULT 'open', - ai_model TEXT, - ai_reason TEXT, - strategy_id TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - closed_at DATETIME - )`, - `CREATE TABLE IF NOT EXISTS conversations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - role TEXT NOT NULL, - content TEXT NOT NULL, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - `CREATE TABLE IF NOT EXISTS user_preferences ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - key TEXT NOT NULL, - value TEXT NOT NULL, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, - UNIQUE(user_id, key) - )`, - `CREATE TABLE IF NOT EXISTS strategy_performance ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - strategy_id TEXT NOT NULL UNIQUE, - strategy_name TEXT, - total_trades INTEGER DEFAULT 0, - win_rate REAL DEFAULT 0, - total_pnl REAL DEFAULT 0, - max_drawdown REAL DEFAULT 0, - sharpe_ratio REAL DEFAULT 0, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - `CREATE TABLE IF NOT EXISTS market_snapshots ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - symbol TEXT NOT NULL, - price REAL NOT NULL, - volume_24h REAL, - change_24h REAL, - note TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP - )`, - `CREATE INDEX IF NOT EXISTS idx_trades_symbol ON trades(symbol)`, - `CREATE INDEX IF NOT EXISTS idx_trades_status ON trades(status)`, - `CREATE INDEX IF NOT EXISTS idx_conversations_user ON conversations(user_id, created_at)`, - `CREATE INDEX IF NOT EXISTS idx_snapshots_symbol ON market_snapshots(symbol, created_at)`, - } - - for _, q := range queries { - if _, err := s.db.Exec(q); err != nil { - return fmt.Errorf("exec %q: %w", q[:40], err) - } - } - return nil -} - -// --- Trade Operations --- - -// SaveTrade inserts a new trade record. -func (s *Store) SaveTrade(t *TradeRecord) (int64, error) { - res, err := s.db.Exec( - `INSERT INTO trades (exchange, symbol, side, type, price, quantity, pnl, fee, status, ai_model, ai_reason, strategy_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, - t.Exchange, t.Symbol, t.Side, t.Type, t.Price, t.Quantity, - t.PnL, t.Fee, t.Status, t.AIModel, t.AIReason, t.StrategyID, - ) - if err != nil { - return 0, err - } - return res.LastInsertId() -} - -// GetRecentTrades returns the last N trades. -func (s *Store) GetRecentTrades(limit int) ([]TradeRecord, error) { - rows, err := s.db.Query( - `SELECT id, exchange, symbol, side, type, price, quantity, pnl, fee, status, - COALESCE(ai_model,''), COALESCE(ai_reason,''), COALESCE(strategy_id,''), created_at, closed_at - FROM trades ORDER BY created_at DESC LIMIT ?`, limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - var trades []TradeRecord - for rows.Next() { - var t TradeRecord - var closedAt sql.NullTime - if err := rows.Scan(&t.ID, &t.Exchange, &t.Symbol, &t.Side, &t.Type, - &t.Price, &t.Quantity, &t.PnL, &t.Fee, &t.Status, - &t.AIModel, &t.AIReason, &t.StrategyID, &t.CreatedAt, &closedAt); err != nil { - return nil, err - } - if closedAt.Valid { - t.ClosedAt = &closedAt.Time - } - trades = append(trades, t) - } - return trades, nil -} - -// --- Conversation Operations --- - -// SaveMessage stores a conversation message. -func (s *Store) SaveMessage(userID int64, role, content string) error { - _, err := s.db.Exec( - `INSERT INTO conversations (user_id, role, content) VALUES (?, ?, ?)`, - userID, role, content, - ) - return err -} - -// GetRecentMessages returns the last N messages for a user. -func (s *Store) GetRecentMessages(userID int64, limit int) ([]Conversation, error) { - rows, err := s.db.Query( - `SELECT id, user_id, role, content, created_at FROM conversations - WHERE user_id = ? ORDER BY created_at DESC LIMIT ?`, userID, limit, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - var msgs []Conversation - for rows.Next() { - var c Conversation - if err := rows.Scan(&c.ID, &c.UserID, &c.Role, &c.Content, &c.CreatedAt); err != nil { - return nil, err - } - msgs = append(msgs, c) - } - - // Reverse to get chronological order - for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 { - msgs[i], msgs[j] = msgs[j], msgs[i] - } - return msgs, nil -} - -// --- Preference Operations --- - -// SetPreference upserts a user preference. -func (s *Store) SetPreference(userID int64, key, value string) error { - _, err := s.db.Exec( - `INSERT INTO user_preferences (user_id, key, value, updated_at) - VALUES (?, ?, ?, ?) - ON CONFLICT(user_id, key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`, - userID, key, value, time.Now(), - ) - return err -} - -// GetPreference retrieves a user preference. -func (s *Store) GetPreference(userID int64, key string) (string, error) { - var value string - err := s.db.QueryRow( - `SELECT value FROM user_preferences WHERE user_id = ? AND key = ?`, - userID, key, - ).Scan(&value) - if err == sql.ErrNoRows { - return "", nil - } - return value, err -} diff --git a/nofxi/internal/perception/market.go b/nofxi/internal/perception/market.go deleted file mode 100644 index 4bf5dc80..00000000 --- a/nofxi/internal/perception/market.go +++ /dev/null @@ -1,214 +0,0 @@ -package perception - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "sync" - "time" -) - -// PriceAlert triggers when a symbol hits a threshold. -type PriceAlert struct { - Symbol string - Price float64 - Direction string // "above" or "below" - Threshold float64 - Triggered time.Time -} - -// AlertCallback is called when a price alert triggers. -type AlertCallback func(alert PriceAlert) - -// MarketMonitor watches symbols and detects price anomalies. -type MarketMonitor struct { - mu sync.RWMutex - watching map[string]*watchState - alerts []alertRule - onAlert AlertCallback - httpClient *http.Client - logger *slog.Logger - stopCh chan struct{} - pollInterval time.Duration -} - -type watchState struct { - Symbol string - LastPrice float64 - Change1h float64 - Change24h float64 - UpdatedAt time.Time -} - -type alertRule struct { - Symbol string - Direction string // "above" or "below" - Threshold float64 - Fired bool -} - -// NewMarketMonitor creates a new market monitor. -func NewMarketMonitor(onAlert AlertCallback, logger *slog.Logger) *MarketMonitor { - return &MarketMonitor{ - watching: make(map[string]*watchState), - onAlert: onAlert, - httpClient: &http.Client{Timeout: 10 * time.Second}, - logger: logger, - stopCh: make(chan struct{}), - pollInterval: 30 * time.Second, - } -} - -// Watch starts monitoring a symbol. -func (m *MarketMonitor) Watch(symbol string) { - m.mu.Lock() - defer m.mu.Unlock() - m.watching[symbol] = &watchState{Symbol: symbol} - m.logger.Info("watching symbol", "symbol", symbol) -} - -// Unwatch stops monitoring a symbol. -func (m *MarketMonitor) Unwatch(symbol string) { - m.mu.Lock() - defer m.mu.Unlock() - delete(m.watching, symbol) -} - -// AddAlert adds a price alert rule. -func (m *MarketMonitor) AddAlert(symbol, direction string, threshold float64) { - m.mu.Lock() - defer m.mu.Unlock() - m.alerts = append(m.alerts, alertRule{ - Symbol: symbol, - Direction: direction, - Threshold: threshold, - }) - m.logger.Info("alert added", "symbol", symbol, "direction", direction, "threshold", threshold) -} - -// GetSnapshot returns the latest state for a symbol. -func (m *MarketMonitor) GetSnapshot(symbol string) (*watchState, bool) { - m.mu.RLock() - defer m.mu.RUnlock() - s, ok := m.watching[symbol] - return s, ok -} - -// GetAllSnapshots returns all watched symbols. -func (m *MarketMonitor) GetAllSnapshots() map[string]*watchState { - m.mu.RLock() - defer m.mu.RUnlock() - result := make(map[string]*watchState) - for k, v := range m.watching { - result[k] = v - } - return result -} - -// Start begins the monitoring loop. -func (m *MarketMonitor) Start() { - go m.loop() -} - -// Stop stops the monitoring loop. -func (m *MarketMonitor) Stop() { - close(m.stopCh) -} - -func (m *MarketMonitor) loop() { - ticker := time.NewTicker(m.pollInterval) - defer ticker.Stop() - - // Initial fetch - m.updatePrices() - - for { - select { - case <-m.stopCh: - return - case <-ticker.C: - m.updatePrices() - } - } -} - -func (m *MarketMonitor) updatePrices() { - m.mu.RLock() - symbols := make([]string, 0, len(m.watching)) - for s := range m.watching { - symbols = append(symbols, s) - } - m.mu.RUnlock() - - for _, symbol := range symbols { - price, err := m.fetchPrice(symbol) - if err != nil { - m.logger.Error("fetch price", "symbol", symbol, "error", err) - continue - } - - m.mu.Lock() - if state, ok := m.watching[symbol]; ok { - state.LastPrice = price - state.UpdatedAt = time.Now() - } - - // Check alerts - for i := range m.alerts { - a := &m.alerts[i] - if a.Symbol != symbol || a.Fired { - continue - } - triggered := false - if a.Direction == "above" && price >= a.Threshold { - triggered = true - } else if a.Direction == "below" && price <= a.Threshold { - triggered = true - } - if triggered { - a.Fired = true - if m.onAlert != nil { - m.onAlert(PriceAlert{ - Symbol: symbol, - Price: price, - Direction: a.Direction, - Threshold: a.Threshold, - Triggered: time.Now(), - }) - } - } - } - m.mu.Unlock() - } -} - -// fetchPrice gets current price from Binance public API (no auth needed). -func (m *MarketMonitor) fetchPrice(symbol string) (float64, error) { - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/price?symbol=%s", symbol) - resp, err := m.httpClient.Get(url) - if err != nil { - return 0, err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return 0, err - } - - var result struct { - Price string `json:"price"` - } - if err := json.Unmarshal(body, &result); err != nil { - return 0, err - } - - var price float64 - fmt.Sscanf(result.Price, "%f", &price) - if price == 0 { - return 0, fmt.Errorf("invalid price for %s", symbol) - } - return price, nil -} diff --git a/nofxi/internal/perception/news.go b/nofxi/internal/perception/news.go deleted file mode 100644 index fef77f82..00000000 --- a/nofxi/internal/perception/news.go +++ /dev/null @@ -1,136 +0,0 @@ -package perception - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "net/http" - "strings" - "time" -) - -// NewsItem represents a crypto news headline. -type NewsItem struct { - Title string `json:"title"` - Source string `json:"source"` - URL string `json:"url"` - Sentiment string `json:"sentiment"` // "bullish", "bearish", "neutral" - Symbols []string `json:"symbols"` // Related symbols - Timestamp time.Time `json:"timestamp"` -} - -// NewsMonitor fetches crypto news and detects sentiment shifts. -type NewsMonitor struct { - httpClient *http.Client - logger *slog.Logger - lastCheck time.Time - seenURLs map[string]bool -} - -// NewNewsMonitor creates a new news monitor. -func NewNewsMonitor(logger *slog.Logger) *NewsMonitor { - return &NewsMonitor{ - httpClient: &http.Client{Timeout: 15 * time.Second}, - logger: logger, - seenURLs: make(map[string]bool), - } -} - -// FetchNews gets recent crypto news from CryptoCompare (free, no auth needed). -func (n *NewsMonitor) FetchNews() ([]NewsItem, error) { - url := "https://min-api.cryptocompare.com/data/v2/news/?lang=EN&sortOrder=latest" - resp, err := n.httpClient.Get(url) - if err != nil { - return nil, fmt.Errorf("fetch news: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var result struct { - Data []struct { - Title string `json:"title"` - Source string `json:"source"` - URL string `json:"url"` - Body string `json:"body"` - Categories string `json:"categories"` - PublishedOn int64 `json:"published_on"` - } `json:"Data"` - } - if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("parse news: %w", err) - } - - var items []NewsItem - for _, d := range result.Data { - if n.seenURLs[d.URL] { - continue - } - n.seenURLs[d.URL] = true - - item := NewsItem{ - Title: d.Title, - Source: d.Source, - URL: d.URL, - Sentiment: classifySentiment(d.Title + " " + d.Body), - Symbols: extractSymbols(d.Title + " " + d.Categories), - Timestamp: time.Unix(d.PublishedOn, 0), - } - items = append(items, item) - } - - // Keep seen URLs map from growing forever - if len(n.seenURLs) > 1000 { - n.seenURLs = make(map[string]bool) - } - - n.lastCheck = time.Now() - return items, nil -} - -// classifySentiment does basic keyword-based sentiment analysis. -func classifySentiment(text string) string { - lower := strings.ToLower(text) - - bullish := []string{"surge", "rally", "soar", "bullish", "breakout", "all-time high", "ath", - "pump", "moon", "gain", "rise", "uptrend", "buy signal", "accumulate", "adoption"} - bearish := []string{"crash", "dump", "plunge", "bearish", "sell-off", "selloff", "decline", - "drop", "fall", "liquidat", "hack", "exploit", "ban", "fraud", "scam", "risk"} - - bullCount, bearCount := 0, 0 - for _, w := range bullish { - if strings.Contains(lower, w) { - bullCount++ - } - } - for _, w := range bearish { - if strings.Contains(lower, w) { - bearCount++ - } - } - - if bullCount > bearCount { - return "bullish" - } - if bearCount > bullCount { - return "bearish" - } - return "neutral" -} - -// extractSymbols finds crypto symbols mentioned in text. -func extractSymbols(text string) []string { - upper := strings.ToUpper(text) - known := []string{"BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "ADA", "AVAX", "DOT", "LINK", "MATIC", "UNI", "AAVE"} - var found []string - for _, s := range known { - if strings.Contains(upper, s) { - found = append(found, s) - } - } - return found -} diff --git a/nofxi/internal/perception/perception.go b/nofxi/internal/perception/perception.go deleted file mode 100644 index 0957027b..00000000 --- a/nofxi/internal/perception/perception.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package perception implements the Perception Layer. -// -// Phase 2 — stubs for now. -// -// Planned capabilities: -// - Real-time market data monitoring -// - Price anomaly detection -// - Position tracking and P/L monitoring -// - News and sentiment analysis -// - Custom alert triggers -package perception - -// Monitor is the market perception interface. -type Monitor interface { - // WatchSymbol starts monitoring a symbol for price changes. - WatchSymbol(symbol string) error - - // Stop stops all monitoring. - Stop() -} diff --git a/nofxi/internal/perception/sentinel.go b/nofxi/internal/perception/sentinel.go deleted file mode 100644 index 67d772ec..00000000 --- a/nofxi/internal/perception/sentinel.go +++ /dev/null @@ -1,282 +0,0 @@ -package perception - -import ( - "encoding/json" - "fmt" - "io" - "log/slog" - "math" - "net/http" - "strconv" - "sync" - "time" -) - -// Signal types for proactive notifications. -type SignalType string - -const ( - SignalPriceBreakout SignalType = "price_breakout" // Sudden price move - SignalVolumeSpike SignalType = "volume_spike" // Abnormal volume - SignalFundingRate SignalType = "funding_rate" // Extreme funding rate - SignalLiquidation SignalType = "liquidation_wave" // Mass liquidations - SignalTrendReversal SignalType = "trend_reversal" // Potential reversal - SignalPositionRisk SignalType = "position_risk" // User's position at risk -) - -// Signal is a proactive market event detected by the sentinel. -type Signal struct { - Type SignalType `json:"type"` - Symbol string `json:"symbol"` - Severity string `json:"severity"` // "info", "warning", "critical" - Title string `json:"title"` - Detail string `json:"detail"` - Price float64 `json:"price"` - Change float64 `json:"change"` // Percentage - Timestamp time.Time `json:"timestamp"` -} - -// SignalCallback is called when the sentinel detects something. -type SignalCallback func(signal Signal) - -// Sentinel continuously monitors markets and detects anomalies. -// This is the "eyes" of NOFXi — always watching, always analyzing. -type Sentinel struct { - mu sync.RWMutex - symbols []string - history map[string][]pricePoint // symbol → recent prices - onSignal SignalCallback - httpClient *http.Client - logger *slog.Logger - stopCh chan struct{} - - // Thresholds - priceBreakoutPct float64 // Price move % to trigger alert (default 3%) - volumeSpikeMult float64 // Volume multiplier vs average (default 3x) - fundingThreshold float64 // Extreme funding rate threshold (default 0.1%) -} - -type pricePoint struct { - Price float64 - Volume float64 - Timestamp time.Time -} - -// NewSentinel creates a new market sentinel. -func NewSentinel(symbols []string, onSignal SignalCallback, logger *slog.Logger) *Sentinel { - return &Sentinel{ - symbols: symbols, - history: make(map[string][]pricePoint), - onSignal: onSignal, - httpClient: &http.Client{Timeout: 10 * time.Second}, - logger: logger, - stopCh: make(chan struct{}), - priceBreakoutPct: 3.0, - volumeSpikeMult: 3.0, - fundingThreshold: 0.1, - } -} - -// Start begins the sentinel loop. Checks every 60 seconds. -func (s *Sentinel) Start() { - go s.loop() - s.logger.Info("sentinel started", "symbols", s.symbols) -} - -// Stop stops the sentinel. -func (s *Sentinel) Stop() { - close(s.stopCh) -} - -// AddSymbol adds a symbol to watch. -func (s *Sentinel) AddSymbol(symbol string) { - s.mu.Lock() - defer s.mu.Unlock() - for _, sym := range s.symbols { - if sym == symbol { - return - } - } - s.symbols = append(s.symbols, symbol) -} - -func (s *Sentinel) loop() { - ticker := time.NewTicker(60 * time.Second) - defer ticker.Stop() - - // Initial scan - s.scan() - - for { - select { - case <-s.stopCh: - return - case <-ticker.C: - s.scan() - } - } -} - -func (s *Sentinel) scan() { - s.mu.RLock() - symbols := make([]string, len(s.symbols)) - copy(symbols, s.symbols) - s.mu.RUnlock() - - for _, sym := range symbols { - s.checkSymbol(sym) - } - s.checkFundingRates() -} - -func (s *Sentinel) checkSymbol(symbol string) { - // Fetch current ticker - ticker, err := s.fetchTicker(symbol) - if err != nil { - return - } - - price, _ := strconv.ParseFloat(ticker["lastPrice"].(string), 64) - volume, _ := strconv.ParseFloat(ticker["quoteVolume"].(string), 64) - changePct, _ := strconv.ParseFloat(ticker["priceChangePercent"].(string), 64) - - now := time.Now() - point := pricePoint{Price: price, Volume: volume, Timestamp: now} - - s.mu.Lock() - hist := s.history[symbol] - hist = append(hist, point) - // Keep last 60 points (1 hour at 1min intervals) - if len(hist) > 60 { - hist = hist[len(hist)-60:] - } - s.history[symbol] = hist - s.mu.Unlock() - - // Need at least 5 data points to detect anomalies - if len(hist) < 5 { - return - } - - // === Detect Price Breakout === - // Compare current price to 5-minute-ago price - fiveAgo := hist[len(hist)-5] - pctMove := ((price - fiveAgo.Price) / fiveAgo.Price) * 100 - if math.Abs(pctMove) >= s.priceBreakoutPct { - direction := "📈 上涨" - severity := "warning" - if pctMove < 0 { - direction = "📉 下跌" - } - if math.Abs(pctMove) >= s.priceBreakoutPct*2 { - severity = "critical" - } - s.emit(Signal{ - Type: SignalPriceBreakout, - Symbol: symbol, - Severity: severity, - Title: fmt.Sprintf("%s %s 急速%s %.1f%%", symbol, direction, map[bool]string{true: "拉升", false: "下跌"}[pctMove > 0], math.Abs(pctMove)), - Detail: fmt.Sprintf("5分钟内从 $%.2f → $%.2f,变动 %.1f%%\n24h 涨跌: %.1f%%", fiveAgo.Price, price, pctMove, changePct), - Price: price, - Change: pctMove, - }) - } - - // === Detect Volume Spike === - if len(hist) >= 10 { - var avgVol float64 - for i := 0; i < len(hist)-1; i++ { - avgVol += hist[i].Volume - } - avgVol /= float64(len(hist) - 1) - if avgVol > 0 && volume > avgVol*s.volumeSpikeMult { - mult := volume / avgVol - s.emit(Signal{ - Type: SignalVolumeSpike, - Symbol: symbol, - Severity: "warning", - Title: fmt.Sprintf("%s 成交量异常放大 %.1fx", symbol, mult), - Detail: fmt.Sprintf("当前成交量是平均值的 %.1f 倍\n价格: $%.2f (24h: %.1f%%)", mult, price, changePct), - Price: price, - Change: changePct, - }) - } - } -} - -func (s *Sentinel) checkFundingRates() { - url := "https://fapi.binance.com/fapi/v1/premiumIndex" - resp, err := s.httpClient.Get(url) - if err != nil { - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return - } - - var indexes []map[string]interface{} - if err := json.Unmarshal(body, &indexes); err != nil { - return - } - - s.mu.RLock() - watchSet := make(map[string]bool) - for _, sym := range s.symbols { - watchSet[sym] = true - } - s.mu.RUnlock() - - for _, idx := range indexes { - symbol, _ := idx["symbol"].(string) - if !watchSet[symbol] { - continue - } - rateStr, _ := idx["lastFundingRate"].(string) - rate, _ := strconv.ParseFloat(rateStr, 64) - ratePct := rate * 100 - - if math.Abs(ratePct) >= s.fundingThreshold { - direction := "多头主导" - if ratePct < 0 { - direction = "空头主导" - } - s.emit(Signal{ - Type: SignalFundingRate, - Symbol: symbol, - Severity: "info", - Title: fmt.Sprintf("%s 资金费率异常: %.4f%%", symbol, ratePct), - Detail: fmt.Sprintf("当前资金费率 %.4f%% (%s)\n极端费率可能预示反转", ratePct, direction), - Change: ratePct, - }) - } - } -} - -func (s *Sentinel) fetchTicker(symbol string) (map[string]interface{}, error) { - url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/ticker/24hr?symbol=%s", symbol) - resp, err := s.httpClient.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) - var result map[string]interface{} - json.Unmarshal(body, &result) - return result, nil -} - -func (s *Sentinel) emit(sig Signal) { - sig.Timestamp = time.Now() - s.logger.Info("signal detected", - "type", sig.Type, - "symbol", sig.Symbol, - "severity", sig.Severity, - "title", sig.Title, - ) - if s.onSignal != nil { - s.onSignal(sig) - } -} diff --git a/nofxi/internal/thinking/engine.go b/nofxi/internal/thinking/engine.go deleted file mode 100644 index 4c4e73dc..00000000 --- a/nofxi/internal/thinking/engine.go +++ /dev/null @@ -1,28 +0,0 @@ -package thinking - -import "context" - -// Engine is the AI decision-making interface. -type Engine interface { - // Chat sends a message to the LLM with conversation context and returns a response. - Chat(ctx context.Context, messages []Message) (string, error) - - // Analyze asks the AI to analyze market data and provide a trading recommendation. - Analyze(ctx context.Context, prompt string) (*Analysis, error) -} - -// Message represents a chat message. -type Message struct { - Role string `json:"role"` // "system", "user", "assistant" - Content string `json:"content"` -} - -// Analysis holds an AI trading recommendation. -type Analysis struct { - Action string `json:"action"` // "buy", "sell", "hold", "wait" - Symbol string `json:"symbol"` - Confidence float64 `json:"confidence"` // 0.0 - 1.0 - Reasoning string `json:"reasoning"` - StopLoss float64 `json:"stop_loss,omitempty"` - TakeProfit float64 `json:"take_profit,omitempty"` -} diff --git a/nofxi/internal/thinking/llm.go b/nofxi/internal/thinking/llm.go deleted file mode 100644 index 04321439..00000000 --- a/nofxi/internal/thinking/llm.go +++ /dev/null @@ -1,161 +0,0 @@ -package thinking - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -// LLMEngine implements Engine using an OpenAI-compatible API. -// Works with OpenAI, claw402 (x402), DeepSeek, Dashscope (Qwen), etc. -type LLMEngine struct { - baseURL string - apiKey string - model string - httpClient *http.Client -} - -// NewLLMEngine creates a new LLM-backed thinking engine. -func NewLLMEngine(baseURL, apiKey, model string) *LLMEngine { - if baseURL == "" { - baseURL = "https://api.openai.com/v1" - } - return &LLMEngine{ - baseURL: baseURL, - apiKey: apiKey, - model: model, - httpClient: &http.Client{ - Timeout: 60 * time.Second, - }, - } -} - -// chatRequest is the OpenAI chat completions request body. -type chatRequest struct { - Model string `json:"model"` - Messages []Message `json:"messages"` -} - -// chatResponse handles both standard and thinking-mode responses. -type chatResponse struct { - Choices []struct { - Message struct { - Content *string `json:"content"` // Can be null in thinking mode - ReasoningContent string `json:"reasoning_content"` // Qwen3 thinking mode - } `json:"message"` - } `json:"choices"` - Error *struct { - Message string `json:"message"` - } `json:"error,omitempty"` -} - -// Chat sends messages to the LLM and returns the response. -func (e *LLMEngine) Chat(ctx context.Context, messages []Message) (string, error) { - reqBody := chatRequest{ - Model: e.model, - Messages: messages, - } - - body, err := json.Marshal(reqBody) - if err != nil { - return "", fmt.Errorf("marshal request: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, "POST", e.baseURL+"/chat/completions", bytes.NewReader(body)) - if err != nil { - return "", fmt.Errorf("create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - if e.apiKey != "" { - req.Header.Set("Authorization", "Bearer "+e.apiKey) - } - - resp, err := e.httpClient.Do(req) - if err != nil { - return "", fmt.Errorf("http request: %w", err) - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("LLM API error (status %d): %s", resp.StatusCode, string(respBody)) - } - - var chatResp chatResponse - if err := json.Unmarshal(respBody, &chatResp); err != nil { - return "", fmt.Errorf("unmarshal response: %w", err) - } - - if chatResp.Error != nil { - return "", fmt.Errorf("LLM error: %s", chatResp.Error.Message) - } - - if len(chatResp.Choices) == 0 { - return "", fmt.Errorf("LLM returned no choices") - } - - // Extract content — handle thinking mode where content can be null - choice := chatResp.Choices[0] - content := "" - if choice.Message.Content != nil { - content = *choice.Message.Content - } - - // If content is empty but reasoning_content exists, use that - if content == "" && choice.Message.ReasoningContent != "" { - content = choice.Message.ReasoningContent - } - - if content == "" { - return "🤔 (AI returned empty response)", nil - } - - return content, nil -} - -// Analyze sends an analysis prompt and parses the AI response. -func (e *LLMEngine) Analyze(ctx context.Context, prompt string) (*Analysis, error) { - systemPrompt := `You are NOFXi, an expert AI trading analyst. Analyze the given market data and provide a trading recommendation. - -Respond in JSON format: -{ - "action": "buy|sell|hold|wait", - "symbol": "BTC/USDT", - "confidence": 0.85, - "reasoning": "Brief explanation", - "stop_loss": 0.0, - "take_profit": 0.0 -} - -Be concise. Only recommend high-confidence trades.` - - messages := []Message{ - {Role: "system", Content: systemPrompt}, - {Role: "user", Content: prompt}, - } - - resp, err := e.Chat(ctx, messages) - if err != nil { - return nil, err - } - - var analysis Analysis - if err := json.Unmarshal([]byte(resp), &analysis); err != nil { - // If JSON parsing fails, return the raw text as reasoning - return &Analysis{ - Action: "hold", - Reasoning: resp, - }, nil - } - - return &analysis, nil -} diff --git a/nofxi/internal/thinking/thinking.go b/nofxi/internal/thinking/thinking.go deleted file mode 100644 index aabe28ca..00000000 --- a/nofxi/internal/thinking/thinking.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package thinking implements the Thinking Layer. -// -// Provides AI-powered decision making via OpenAI-compatible APIs. -// Supports multiple providers: OpenAI, claw402 (x402), DeepSeek, Qwen, etc. -// -// Key interfaces: -// - Engine: core AI decision interface (Chat + Analyze) -// - LLMEngine: concrete implementation using HTTP/REST -package thinking diff --git a/nofxi/web/index.html b/nofxi/web/index.html deleted file mode 100644 index a34b1fb6..00000000 --- a/nofxi/web/index.html +++ /dev/null @@ -1,418 +0,0 @@ - - - - - - NOFXi — AI Trading Agent - - - - - - - -
-
- -
- - - -
-
-
-
--
- -
-
- - -
- -
-
-
-
Analysis
-
- - - - -
-
-
-
Portfolio
-
- - - -
-
-
-
Monitor
-
- - -
-
-
-
Strategy
-
- - -
-
-
- - -
-
-
Thinking...
-
-
- - -
-
Enter to send · /help, /analyze BTC, /buy ETH 0.1
-
-
- - -
-
-
-
- -
- -- - -- -
-
- - - - - - -
-
-
-
-
- - - -