Files
nofx/nofxi/internal/agent/strategy.go
shinchan-zhai 34f5e6fe71 feat(nofxi): Phase 3 complete - Exchange factory, Web UI, Strategy runner, Docker
Exchange Factory:
- CreateTrader() supports Binance/OKX/Bybit/Bitget/KuCoin/Gate
- Auto-registers traders from config on startup
- Direct import of nofx/trader packages (merged into main module)

Web UI:
- Dark theme chat interface at :8900
- Quick action sidebar (analyze, watch, positions, balance)
- Real-time health check indicator
- Mobile responsive

Strategy Runner:
- /strategy start BTC 1h - AI auto-analyzes on interval
- /strategy stop <id> - stop strategy
- /strategy list - view active strategies
- Notifications pushed to Telegram on signals
- Configurable intervals: 15m/30m/1h/4h

Docker:
- Multi-stage Dockerfile (alpine, ~20MB)
- docker-compose.yml with volume persistence

Bug fixes:
- Fixed panic on empty exchanges config
- Fixed thinking mode response parsing (qwen3 content:null)
- Added request timeout (55s) in Telegram handler
- Better error logging
2026-03-22 22:04:37 +08:00

200 lines
4.9 KiB
Go

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 <id>`")
return sb.String()
}