mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-07 21:12:00 +08:00
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
200 lines
4.9 KiB
Go
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()
|
|
}
|