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.
-
-
-
-
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Enter to send · /help, /analyze BTC, /buy ETH 0.1
-
-
-
-
-
-
-
-
-
-
-
--
-
--
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-