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
This commit is contained in:
shinchan-zhai
2026-03-22 22:04:37 +08:00
parent cf7bf16c28
commit 34f5e6fe71
16 changed files with 784 additions and 87 deletions

View File

@@ -11,7 +11,7 @@ import (
)
// LLMEngine implements Engine using an OpenAI-compatible API.
// Works with OpenAI, claw402 (x402), DeepSeek, Qwen, etc.
// Works with OpenAI, claw402 (x402), DeepSeek, Dashscope (Qwen), etc.
type LLMEngine struct {
baseURL string
apiKey string
@@ -29,7 +29,7 @@ func NewLLMEngine(baseURL, apiKey, model string) *LLMEngine {
apiKey: apiKey,
model: model,
httpClient: &http.Client{
Timeout: 120 * time.Second,
Timeout: 60 * time.Second,
},
}
}
@@ -40,11 +40,12 @@ type chatRequest struct {
Messages []Message `json:"messages"`
}
// chatResponse is the OpenAI chat completions response body.
// chatResponse handles both standard and thinking-mode responses.
type chatResponse struct {
Choices []struct {
Message struct {
Content string `json:"content"`
Content *string `json:"content"` // Can be null in thinking mode
ReasoningContent string `json:"reasoning_content"` // Qwen3 thinking mode
} `json:"message"`
} `json:"choices"`
Error *struct {
@@ -102,7 +103,23 @@ func (e *LLMEngine) Chat(ctx context.Context, messages []Message) (string, error
return "", fmt.Errorf("LLM returned no choices")
}
return chatResp.Choices[0].Message.Content, nil
// 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.