mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-19 02:14:35 +08:00
merge: Sync with NoFxAiOS/dev - adopt WebSocket architecture
Merged 54 commits from upstream (NoFxAiOS/dev @8832557) ## Key Changes ### 🚀 WebSocket Real-time Data Architecture - ✅ NEW: WebSocket client with auto-reconnect (websocket_client.go) - ✅ NEW: Market data monitor with combined streams (monitor.go) - ✅ NEW: K-line caching system (combined_streams.go) - ✅ UPGRADE: market/data.go now uses WebSocket instead of REST API - 🎯 Benefits: Lower latency, reduced API limits, auto-reconnect ### 📝 Documentation & CI/CD - ✅ NEW: Comprehensive troubleshooting guides (EN + ZH-CN) - ✅ UPGRADE: Enhanced PR workflow with checks - ✅ NEW: PR title guide & template improvements - ✅ UPDATE: FAQ expanded with common issues ### 🔧 Conflict Resolution - market/data.go: Adopted upstream WebSocket version - prompts/adaptive.txt: Kept our v5.5.6.1 (conf≥85, strict strategy) ### 📊 Stats - Files changed: 40+ - New files: 12 (WebSocket modules, docs, CI) - Commits merged: 54 Related: feature/partial-close-dynamic-tpsl Upstream: NoFxAiOS/nofx @8832557
This commit is contained in:
152
.github/ISSUE_TEMPLATE/bug_report.md
vendored
152
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -6,42 +6,162 @@ labels: bug
|
|||||||
assignees: ''
|
assignees: ''
|
||||||
---
|
---
|
||||||
|
|
||||||
|
> **⚠️ Before submitting:** Please check the [Troubleshooting Guide](../../docs/guides/TROUBLESHOOTING.md) ([中文版](../../docs/guides/TROUBLESHOOTING.zh-CN.md)) to see if your issue can be resolved quickly.
|
||||||
|
|
||||||
## 🐛 Bug Description
|
## 🐛 Bug Description
|
||||||
<!-- A clear and concise description of what the bug is -->
|
<!-- A clear and concise description of what the bug is -->
|
||||||
|
|
||||||
|
|
||||||
|
## 🔍 Bug Category
|
||||||
|
<!-- Check the category that best describes this bug -->
|
||||||
|
- [ ] Trading execution (orders not executing, wrong position size, etc.)
|
||||||
|
- [ ] AI decision issues (unexpected decisions, only opening one direction, etc.)
|
||||||
|
- [ ] Exchange connection (API errors, authentication failures, etc.)
|
||||||
|
- [ ] UI/Frontend (display issues, buttons not working, data not updating, etc.)
|
||||||
|
- [ ] Backend/API (server errors, crashes, performance issues, etc.)
|
||||||
|
- [ ] Configuration (settings not saving, database errors, etc.)
|
||||||
|
- [ ] Other: _________________
|
||||||
|
|
||||||
## 📋 Steps to Reproduce
|
## 📋 Steps to Reproduce
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '...'
|
2. Click on '...' / Run command '...'
|
||||||
3. Run command '...'
|
3. Configure '...'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
## ✅ Expected Behavior
|
## ✅ Expected Behavior
|
||||||
<!-- What you expected to happen -->
|
<!-- What you expected to happen -->
|
||||||
|
|
||||||
|
|
||||||
## ❌ Actual Behavior
|
## ❌ Actual Behavior
|
||||||
<!-- What actually happened -->
|
<!-- What actually happened -->
|
||||||
|
|
||||||
## 📸 Screenshots / Logs
|
|
||||||
<!-- If applicable, add screenshots or error logs to help explain your problem -->
|
|
||||||
|
|
||||||
|
## 📸 Screenshots & Logs
|
||||||
|
|
||||||
|
### Frontend Error (if applicable)
|
||||||
|
<!-- How to capture frontend errors: -->
|
||||||
|
<!-- 1. Open browser DevTools (F12 or Right-click → Inspect) -->
|
||||||
|
<!-- 2. Go to "Console" tab to see JavaScript errors -->
|
||||||
|
<!-- 3. Screenshot the error messages -->
|
||||||
|
<!-- 4. Check "Network" tab for failed API requests (show status code & response) -->
|
||||||
|
|
||||||
|
**Browser Console Screenshot:**
|
||||||
|
<!-- Paste screenshot here -->
|
||||||
|
|
||||||
|
**Network Tab (failed requests):**
|
||||||
|
<!-- Paste screenshot of failed API calls here -->
|
||||||
|
|
||||||
|
### Backend Logs (if applicable)
|
||||||
|
<!-- How to capture backend logs: -->
|
||||||
|
|
||||||
|
**Docker users:**
|
||||||
|
```bash
|
||||||
|
# View backend logs
|
||||||
|
docker compose logs backend --tail=100
|
||||||
|
|
||||||
|
# OR continuously follow logs
|
||||||
|
docker compose logs -f backend
|
||||||
```
|
```
|
||||||
Paste logs here
|
|
||||||
|
**Manual/PM2 users:**
|
||||||
|
```bash
|
||||||
|
# Terminal output where you ran: ./nofx
|
||||||
|
# OR PM2 logs:
|
||||||
|
pm2 logs nofx --lines 100
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend Log Output:**
|
||||||
|
```
|
||||||
|
Paste backend logs here (last 50-100 lines around the error)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Trading/Decision Logs (if trading issue)
|
||||||
|
<!-- Decision logs are saved in: decision_logs/{trader_id}/ -->
|
||||||
|
<!-- Find the latest JSON file and paste relevant parts -->
|
||||||
|
|
||||||
|
**Decision Log Path:** `decision_logs/{trader_id}/{timestamp}.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"paste relevant decision log here if applicable"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## 💻 Environment
|
## 💻 Environment
|
||||||
|
|
||||||
|
**System:**
|
||||||
- **OS:** [e.g. macOS 13, Ubuntu 22.04, Windows 11]
|
- **OS:** [e.g. macOS 13, Ubuntu 22.04, Windows 11]
|
||||||
- **Go Version:** [e.g. 1.21.5]
|
- **Deployment:** [Docker / Manual / PM2]
|
||||||
- **Node.js Version:** [e.g. 18.17.0]
|
|
||||||
- **NOFX Version/Commit:** [e.g. v3.0.0 or commit hash]
|
**Backend:**
|
||||||
- **Deployment Method:** [Docker / Manual / PM2]
|
- **Go Version:** [run: `go version`]
|
||||||
|
- **NOFX Version:** [run: `git log -1 --oneline` or check release tag]
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
- **Browser:** [e.g. Chrome 120, Firefox 121, Safari 17]
|
||||||
|
- **Node.js Version:** [run: `node -v`]
|
||||||
|
|
||||||
|
**Trading Setup:**
|
||||||
- **Exchange:** [Binance / Hyperliquid / Aster]
|
- **Exchange:** [Binance / Hyperliquid / Aster]
|
||||||
|
- **Account Type:** [Main Account / Subaccount]
|
||||||
|
- **Position Mode:** [Hedge Mode (Dual) / One-way Mode] ← **Important for trading bugs!**
|
||||||
|
- **AI Model:** [DeepSeek / Qwen / Custom]
|
||||||
|
- **Number of Traders:** [e.g. 1, 2, etc.]
|
||||||
|
|
||||||
## 🔧 Additional Context
|
## 🔧 Configuration (if relevant)
|
||||||
<!-- Add any other context about the problem here -->
|
<!-- Only include non-sensitive parts of your config -->
|
||||||
|
<!-- ⚠️ NEVER paste API keys or private keys! -->
|
||||||
|
|
||||||
- Does this happen consistently or intermittently?
|
**Leverage Settings:**
|
||||||
- Did it work before? When did it break?
|
```json
|
||||||
- Any recent configuration changes?
|
{
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## ✋ Possible Solution
|
**Any custom settings:**
|
||||||
<!-- Optional: If you have ideas on how to fix this -->
|
<!-- e.g. modified scan_interval, custom coin list, etc. -->
|
||||||
|
|
||||||
|
|
||||||
|
## 📊 Additional Context
|
||||||
|
|
||||||
|
**Frequency:**
|
||||||
|
- [ ] Happens every time
|
||||||
|
- [ ] Happens randomly
|
||||||
|
- [ ] Happened once
|
||||||
|
|
||||||
|
**Timeline:**
|
||||||
|
- Did this work before? [ ] Yes [ ] No
|
||||||
|
- When did it break? [e.g. after upgrade to v3.0.0, after changing config, etc.]
|
||||||
|
- Recent changes? [e.g. updated dependencies, changed exchange, etc.]
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- [ ] System cannot start
|
||||||
|
- [ ] Trading stopped/broken
|
||||||
|
- [ ] UI broken but trading works
|
||||||
|
- [ ] Minor visual issue
|
||||||
|
- [ ] Other: _________________
|
||||||
|
|
||||||
|
## 💡 Possible Solution
|
||||||
|
<!-- Optional: If you have ideas on how to fix this, or workarounds you've tried -->
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Quick Tips for Faster Resolution
|
||||||
|
|
||||||
|
**For Trading Issues:**
|
||||||
|
1. ✅ Check Binance position mode: Go to Futures → ⚙️ Preferences → Position Mode → Must be **Hedge Mode**
|
||||||
|
2. ✅ Verify API permissions: Futures trading must be enabled
|
||||||
|
3. ✅ Check decision logs in `decision_logs/{trader_id}/` for AI reasoning
|
||||||
|
|
||||||
|
**For Connection Issues:**
|
||||||
|
4. ✅ Test API connectivity: `curl http://localhost:8080/api/health`
|
||||||
|
5. ✅ Check API rate limits on exchange
|
||||||
|
6. ✅ Verify API keys are not expired
|
||||||
|
|
||||||
|
**For UI Issues:**
|
||||||
|
7. ✅ Hard refresh: Ctrl+Shift+R (or Cmd+Shift+R on Mac)
|
||||||
|
8. ✅ Check browser console (F12) for errors
|
||||||
|
9. ✅ Verify backend is running: `docker compose ps` or `ps aux | grep nofx`
|
||||||
|
|||||||
322
.github/PR_TITLE_GUIDE.md
vendored
Normal file
322
.github/PR_TITLE_GUIDE.md
vendored
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# PR 标题指南
|
||||||
|
|
||||||
|
## 📋 概述
|
||||||
|
|
||||||
|
我们使用 **Conventional Commits** 格式来保持 PR 标题的一致性,但这是**建议性的**,不会阻止你的 PR 被合并。
|
||||||
|
|
||||||
|
## ✅ 推荐格式
|
||||||
|
|
||||||
|
```
|
||||||
|
type(scope): description
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(trader): add new trading strategy
|
||||||
|
fix(api): resolve authentication issue
|
||||||
|
docs: update README
|
||||||
|
chore(deps): update dependencies
|
||||||
|
ci(workflow): improve GitHub Actions
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 详细说明
|
||||||
|
|
||||||
|
### Type(类型)- 必需
|
||||||
|
|
||||||
|
描述这次变更的类型:
|
||||||
|
|
||||||
|
| Type | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `feat` | 新功能 | `feat(trader): add stop-loss feature` |
|
||||||
|
| `fix` | Bug 修复 | `fix(api): handle null response` |
|
||||||
|
| `docs` | 文档变更 | `docs: update installation guide` |
|
||||||
|
| `style` | 代码格式(不影响代码运行) | `style: format code with prettier` |
|
||||||
|
| `refactor` | 重构(既不是新功能也不是修复) | `refactor(exchange): simplify connection logic` |
|
||||||
|
| `perf` | 性能优化 | `perf(ai): optimize prompt processing` |
|
||||||
|
| `test` | 添加或修改测试 | `test(trader): add unit tests` |
|
||||||
|
| `chore` | 构建过程或辅助工具的变动 | `chore: update dependencies` |
|
||||||
|
| `ci` | CI/CD 相关变更 | `ci: add test coverage report` |
|
||||||
|
| `security` | 安全相关修复 | `security: update vulnerable dependencies` |
|
||||||
|
| `build` | 构建系统或外部依赖项变更 | `build: upgrade webpack to v5` |
|
||||||
|
|
||||||
|
### Scope(范围)- 可选
|
||||||
|
|
||||||
|
描述这次变更影响的范围:
|
||||||
|
|
||||||
|
| Scope | 说明 |
|
||||||
|
|-------|------|
|
||||||
|
| `exchange` | 交易所相关 |
|
||||||
|
| `trader` | 交易员/交易策略 |
|
||||||
|
| `ai` | AI 模型相关 |
|
||||||
|
| `api` | API 接口 |
|
||||||
|
| `ui` | 用户界面 |
|
||||||
|
| `frontend` | 前端代码 |
|
||||||
|
| `backend` | 后端代码 |
|
||||||
|
| `security` | 安全相关 |
|
||||||
|
| `deps` | 依赖项 |
|
||||||
|
| `workflow` | GitHub Actions workflows |
|
||||||
|
| `github` | GitHub 配置 |
|
||||||
|
| `actions` | GitHub Actions |
|
||||||
|
| `config` | 配置文件 |
|
||||||
|
| `docker` | Docker 相关 |
|
||||||
|
| `build` | 构建相关 |
|
||||||
|
| `release` | 发布相关 |
|
||||||
|
|
||||||
|
**注意:** 如果变更影响多个范围,可以省略 scope 或选择最主要的。
|
||||||
|
|
||||||
|
### Description(描述)- 必需
|
||||||
|
|
||||||
|
- 使用现在时态("add" 而不是 "added")
|
||||||
|
- 首字母小写
|
||||||
|
- 结尾不加句号
|
||||||
|
- 简洁明了地描述变更内容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 完整示例
|
||||||
|
|
||||||
|
### ✅ 好的 PR 标题
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(trader): add risk management system
|
||||||
|
fix(exchange): resolve connection timeout issue
|
||||||
|
docs: add API documentation for trading endpoints
|
||||||
|
style: apply consistent code formatting
|
||||||
|
refactor(ai): simplify prompt processing logic
|
||||||
|
perf(backend): optimize database queries
|
||||||
|
test(api): add integration tests for auth
|
||||||
|
chore(deps): update TypeScript to 5.0
|
||||||
|
ci(workflow): add automated security scanning
|
||||||
|
security(api): fix SQL injection vulnerability
|
||||||
|
build(docker): optimize Docker image size
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ 需要改进的标题
|
||||||
|
|
||||||
|
| 不好的标题 | 问题 | 改进后 |
|
||||||
|
|-----------|------|--------|
|
||||||
|
| `update code` | 太模糊 | `refactor(trader): simplify order execution logic` |
|
||||||
|
| `Fixed bug` | 首字母大写,不够具体 | `fix(api): handle edge case in login` |
|
||||||
|
| `Add new feature.` | 有句号,不够具体 | `feat(ui): add dark mode toggle` |
|
||||||
|
| `changes` | 完全不符合格式 | `chore: update dependencies` |
|
||||||
|
| `feat: Added new trading algo` | 时态错误 | `feat(trader): add new trading algorithm` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤖 自动检查行为
|
||||||
|
|
||||||
|
### 当 PR 标题不符合格式时
|
||||||
|
|
||||||
|
1. **不会阻止合并** ✅
|
||||||
|
- 检查会标记为"建议"
|
||||||
|
- PR 仍然可以被审查和合并
|
||||||
|
|
||||||
|
2. **会收到友好提示** 💬
|
||||||
|
- 机器人会在 PR 中留言
|
||||||
|
- 提供格式说明和示例
|
||||||
|
- 建议如何改进标题
|
||||||
|
|
||||||
|
3. **可以随时更新** 🔄
|
||||||
|
- 更新 PR 标题后会重新检查
|
||||||
|
- 无需关闭和重新打开 PR
|
||||||
|
|
||||||
|
### 示例评论
|
||||||
|
|
||||||
|
如果你的 PR 标题是 `update workflow`,你会收到这样的评论:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## ⚠️ PR Title Format Suggestion
|
||||||
|
|
||||||
|
Your PR title doesn't follow the Conventional Commits format,
|
||||||
|
but this won't block your PR from being merged.
|
||||||
|
|
||||||
|
**Current title:** `update workflow`
|
||||||
|
|
||||||
|
**Recommended format:** `type(scope): description`
|
||||||
|
|
||||||
|
### Valid types:
|
||||||
|
feat, fix, docs, style, refactor, perf, test, chore, ci, security, build
|
||||||
|
|
||||||
|
### Common scopes (optional):
|
||||||
|
exchange, trader, ai, api, ui, frontend, backend, security, deps,
|
||||||
|
workflow, github, actions, config, docker, build, release
|
||||||
|
|
||||||
|
### Examples:
|
||||||
|
- feat(trader): add new trading strategy
|
||||||
|
- fix(api): resolve authentication issue
|
||||||
|
- docs: update README
|
||||||
|
- chore(deps): update dependencies
|
||||||
|
- ci(workflow): improve GitHub Actions
|
||||||
|
|
||||||
|
**Note:** This is a suggestion to improve consistency.
|
||||||
|
Your PR can still be reviewed and merged.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 配置详情
|
||||||
|
|
||||||
|
### 支持的 Types
|
||||||
|
|
||||||
|
在 `.github/workflows/pr-checks.yml` 中配置:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
types: |
|
||||||
|
feat
|
||||||
|
fix
|
||||||
|
docs
|
||||||
|
style
|
||||||
|
refactor
|
||||||
|
perf
|
||||||
|
test
|
||||||
|
chore
|
||||||
|
ci
|
||||||
|
security
|
||||||
|
build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 支持的 Scopes
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
scopes: |
|
||||||
|
exchange
|
||||||
|
trader
|
||||||
|
ai
|
||||||
|
api
|
||||||
|
ui
|
||||||
|
frontend
|
||||||
|
backend
|
||||||
|
security
|
||||||
|
deps
|
||||||
|
workflow
|
||||||
|
github
|
||||||
|
actions
|
||||||
|
config
|
||||||
|
docker
|
||||||
|
build
|
||||||
|
release
|
||||||
|
```
|
||||||
|
|
||||||
|
### 添加新的 Scope
|
||||||
|
|
||||||
|
如果你需要添加新的 scope,请:
|
||||||
|
|
||||||
|
1. 在 `.github/workflows/pr-checks.yml` 的 `scopes` 部分添加
|
||||||
|
2. 在 `.github/workflows/pr-checks-run.yml` 更新正则表达式(可选)
|
||||||
|
3. 更新本文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 为什么使用 Conventional Commits?
|
||||||
|
|
||||||
|
### 优点
|
||||||
|
|
||||||
|
1. **自动化 Changelog** 📝
|
||||||
|
- 可以自动生成版本更新日志
|
||||||
|
- 清晰地分类各种变更
|
||||||
|
|
||||||
|
2. **语义化版本** 🔢
|
||||||
|
- `feat` → MINOR 版本(1.1.0)
|
||||||
|
- `fix` → PATCH 版本(1.0.1)
|
||||||
|
- `BREAKING CHANGE` → MAJOR 版本(2.0.0)
|
||||||
|
|
||||||
|
3. **更好的可读性** 👀
|
||||||
|
- 一眼看出 PR 的目的
|
||||||
|
- 更容易浏览 Git 历史
|
||||||
|
|
||||||
|
4. **团队协作** 🤝
|
||||||
|
- 统一的提交风格
|
||||||
|
- 降低沟通成本
|
||||||
|
|
||||||
|
### 示例:自动生成的 Changelog
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## v1.2.0 (2025-11-02)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- **trader**: add risk management system (#123)
|
||||||
|
- **ui**: add dark mode toggle (#125)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
- **api**: resolve authentication issue (#124)
|
||||||
|
- **exchange**: fix connection timeout (#126)
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- update API documentation (#127)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎓 学习资源
|
||||||
|
|
||||||
|
- **Conventional Commits 官网:** https://www.conventionalcommits.org/
|
||||||
|
- **Angular Commit Guidelines:** https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit
|
||||||
|
- **Semantic Versioning:** https://semver.org/
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ FAQ
|
||||||
|
|
||||||
|
### Q: 我必须遵循这个格式吗?
|
||||||
|
|
||||||
|
**A:** 不必须。这是建议性的,不会阻止你的 PR 被合并。但遵循格式可以提高项目的可维护性。
|
||||||
|
|
||||||
|
### Q: 如果我忘记了怎么办?
|
||||||
|
|
||||||
|
**A:** 机器人会在 PR 中提醒你,你可以随时更新标题。
|
||||||
|
|
||||||
|
### Q: 我可以在一个 PR 中做多种类型的变更吗?
|
||||||
|
|
||||||
|
**A:** 可以,但建议:
|
||||||
|
- 选择最主要的类型
|
||||||
|
- 或者考虑拆分成多个 PR(更易于审查)
|
||||||
|
|
||||||
|
### Q: Scope 可以省略吗?
|
||||||
|
|
||||||
|
**A:** 可以。`requireScope: false` 表示 scope 是可选的。
|
||||||
|
|
||||||
|
示例:`docs: update README` (没有 scope 也可以)
|
||||||
|
|
||||||
|
### Q: 我想添加新的 type 或 scope,怎么做?
|
||||||
|
|
||||||
|
**A:** 提一个 PR 修改 `.github/workflows/pr-checks.yml`,并在本文档中说明新增项的用途。
|
||||||
|
|
||||||
|
### Q: Breaking Changes 怎么表示?
|
||||||
|
|
||||||
|
**A:** 在描述中添加 `BREAKING CHANGE:` 或在 type 后加 `!`:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat!: remove deprecated API
|
||||||
|
feat(api)!: change authentication method
|
||||||
|
|
||||||
|
BREAKING CHANGE: The old /auth endpoint is removed
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 统计
|
||||||
|
|
||||||
|
想看项目的 commit 类型分布?运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git log --oneline --no-merges | \
|
||||||
|
grep -oE '^[a-f0-9]+ (feat|fix|docs|style|refactor|perf|test|chore|ci|security|build)' | \
|
||||||
|
cut -d' ' -f2 | sort | uniq -c | sort -rn
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 快速检查清单
|
||||||
|
|
||||||
|
在提交 PR 前,检查你的标题是否:
|
||||||
|
|
||||||
|
- [ ] 包含有效的 type(feat, fix, docs 等)
|
||||||
|
- [ ] 使用小写字母开头
|
||||||
|
- [ ] 使用现在时态("add" 而不是 "added")
|
||||||
|
- [ ] 简洁明了(最好在 50 字符内)
|
||||||
|
- [ ] 准确描述了变更内容
|
||||||
|
|
||||||
|
**记住:** 这些都是建议,不是强制要求!
|
||||||
279
.github/PULL_REQUEST_TEMPLATE.md
vendored
279
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,153 +1,288 @@
|
|||||||
# Pull Request
|
# Pull Request | PR 提交
|
||||||
|
|
||||||
## 📝 Description
|
> **💡 提示 Tip:** 推荐 PR 标题格式 Recommended PR title format: `type(scope): description`
|
||||||
|
> 例如 Examples: `feat(trader): add new strategy` | `fix(api): resolve auth issue`
|
||||||
|
> 详情 Details: [PR Title Guide](./PR_TITLE_GUIDE.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Description | 描述
|
||||||
|
|
||||||
<!-- Provide a brief summary of your changes -->
|
<!-- Provide a brief summary of your changes -->
|
||||||
|
<!-- 简要描述你的变更 -->
|
||||||
|
|
||||||
## 🎯 Type of Change
|
**English:**
|
||||||
|
|
||||||
|
**中文:**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Type of Change | 变更类型
|
||||||
|
|
||||||
<!-- Mark the relevant option with an "x" -->
|
<!-- Mark the relevant option with an "x" -->
|
||||||
|
<!-- 在相关选项上打"x" -->
|
||||||
|
|
||||||
- [ ] 🐛 Bug fix (non-breaking change which fixes an issue)
|
- [ ] 🐛 Bug fix | 修复 Bug(不影响现有功能的修复)
|
||||||
- [ ] ✨ New feature (non-breaking change which adds functionality)
|
- [ ] ✨ New feature | 新功能(不影响现有功能的新增)
|
||||||
- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- [ ] 💥 Breaking change | 破坏性变更(会导致现有功能无法正常工作的修复或功能)
|
||||||
- [ ] 📝 Documentation update
|
- [ ] 📝 Documentation update | 文档更新
|
||||||
- [ ] 🎨 Code style update (formatting, renaming)
|
- [ ] 🎨 Code style update | 代码样式更新(格式化、重命名等)
|
||||||
- [ ] ♻️ Refactoring (no functional changes)
|
- [ ] ♻️ Refactoring | 重构(无功能变更)
|
||||||
- [ ] ⚡ Performance improvement
|
- [ ] ⚡ Performance improvement | 性能优化
|
||||||
- [ ] ✅ Test update
|
- [ ] ✅ Test update | 测试更新
|
||||||
- [ ] 🔧 Build/config change
|
- [ ] 🔧 Build/config change | 构建/配置变更
|
||||||
|
- [ ] 🔒 Security fix | 安全修复
|
||||||
|
|
||||||
## 🔗 Related Issues
|
---
|
||||||
|
|
||||||
|
## 🔗 Related Issues | 相关 Issue
|
||||||
|
|
||||||
<!-- Link related issues below. Use "Closes #123" to auto-close issues when PR is merged -->
|
<!-- Link related issues below. Use "Closes #123" to auto-close issues when PR is merged -->
|
||||||
|
<!-- 在下方关联相关 issue。使用 "Closes #123" 可以在 PR 合并时自动关闭 issue -->
|
||||||
|
|
||||||
- Closes #
|
- Closes # | 关闭 #
|
||||||
- Related to #
|
- Related to # | 相关 #
|
||||||
|
|
||||||
## 📋 Changes Made
|
---
|
||||||
|
|
||||||
|
## 📋 Changes Made | 具体变更
|
||||||
|
|
||||||
<!-- List the specific changes you made -->
|
<!-- List the specific changes you made -->
|
||||||
|
<!-- 列出你做的具体变更 -->
|
||||||
|
|
||||||
|
**English:**
|
||||||
- Change 1
|
- Change 1
|
||||||
- Change 2
|
- Change 2
|
||||||
- Change 3
|
- Change 3
|
||||||
|
|
||||||
## 🧪 Testing
|
**中文:**
|
||||||
|
- 变更 1
|
||||||
|
- 变更 2
|
||||||
|
- 变更 3
|
||||||
|
|
||||||
### Manual Testing
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing | 测试
|
||||||
|
|
||||||
|
### Manual Testing | 手动测试
|
||||||
|
|
||||||
<!-- Describe how you tested your changes -->
|
<!-- Describe how you tested your changes -->
|
||||||
|
<!-- 描述你如何测试你的变更 -->
|
||||||
|
|
||||||
- [ ] Tested locally (manual verification)
|
- [ ] Tested locally | 本地测试通过
|
||||||
- [ ] Tested on testnet (for exchange integrations)
|
- [ ] Tested on testnet | 测试网测试通过(交易所集成相关)
|
||||||
- [ ] Tested with different configurations
|
- [ ] Tested with different configurations | 测试了不同配置
|
||||||
- [ ] Verified no existing functionality broke
|
- [ ] Verified no existing functionality broke | 确认没有破坏现有功能
|
||||||
|
|
||||||
### Test Environment
|
### Test Environment | 测试环境
|
||||||
|
|
||||||
- **OS:** [e.g. macOS, Ubuntu]
|
- **OS | 操作系统:** [e.g. macOS, Ubuntu, Windows]
|
||||||
- **Go Version:** [e.g. 1.21.5]
|
- **Go Version | Go 版本:** [e.g. 1.21.5]
|
||||||
- **Exchange:** [if applicable]
|
- **Node Version | Node 版本:** [e.g. 18.x] (if applicable | 如适用)
|
||||||
|
- **Exchange | 交易所:** [if applicable | 如适用]
|
||||||
|
|
||||||
### Test Results
|
### Test Results | 测试结果
|
||||||
|
|
||||||
<!-- Paste relevant test output or describe results -->
|
<!-- Paste relevant test output or describe results -->
|
||||||
|
<!-- 粘贴相关测试输出或描述结果 -->
|
||||||
|
|
||||||
```
|
```
|
||||||
Test output here
|
Test output here | 测试输出
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📸 Screenshots / Demo
|
---
|
||||||
|
|
||||||
|
## 📸 Screenshots / Demo | 截图/演示
|
||||||
|
|
||||||
<!-- If applicable, add screenshots or video demo -->
|
<!-- If applicable, add screenshots or video demo -->
|
||||||
|
<!-- 如适用,添加截图或视频演示 -->
|
||||||
|
|
||||||
<!-- For UI changes, include before/after screenshots -->
|
<!-- For UI changes, include before/after screenshots -->
|
||||||
|
<!-- 对于 UI 变更,包含变更前后的截图 -->
|
||||||
|
|
||||||
**Before:**
|
**Before | 变更前:**
|
||||||
|
|
||||||
|
|
||||||
**After:**
|
**After | 变更后:**
|
||||||
|
|
||||||
|
|
||||||
## ✅ Checklist
|
---
|
||||||
|
|
||||||
|
## ✅ Checklist | 检查清单
|
||||||
|
|
||||||
<!-- Mark completed items with an "x" -->
|
<!-- Mark completed items with an "x" -->
|
||||||
|
<!-- 在已完成的项目上打"x" -->
|
||||||
|
|
||||||
### Code Quality
|
### Code Quality | 代码质量
|
||||||
|
|
||||||
- [ ] My code follows the project's code style ([Contributing Guide](../CONTRIBUTING.md))
|
- [ ] My code follows the project's code style | 我的代码遵循项目代码风格 ([Contributing Guide](../CONTRIBUTING.md))
|
||||||
- [ ] I have performed a self-review of my code
|
- [ ] I have performed a self-review of my code | 我已进行代码自查
|
||||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
- [ ] I have commented my code, particularly in hard-to-understand areas | 我已添加代码注释,特别是难以理解的部分
|
||||||
- [ ] My changes generate no new warnings or errors
|
- [ ] My changes generate no new warnings or errors | 我的变更没有产生新的警告或错误
|
||||||
- [ ] Code compiles successfully (`go build` / `npm run build`)
|
- [ ] Code compiles successfully | 代码编译成功 (`go build` / `npm run build`)
|
||||||
- [ ] I have run `go fmt` (for Go code)
|
- [ ] I have run `go fmt` (for Go code) | 我已运行 `go fmt`(Go 代码)
|
||||||
|
- [ ] I have run `npm run lint` (for frontend code) | 我已运行 `npm run lint`(前端代码)
|
||||||
|
|
||||||
### Testing
|
### Testing | 测试
|
||||||
|
|
||||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
- [ ] I have added tests that prove my fix is effective or that my feature works | 我已添加证明修复有效或功能正常的测试
|
||||||
- [ ] New and existing unit tests pass locally
|
- [ ] New and existing unit tests pass locally | 新旧单元测试在本地通过
|
||||||
- [ ] I have tested on testnet (for trading/exchange changes)
|
- [ ] I have tested on testnet (for trading/exchange changes) | 我已在测试网测试(交易/交易所变更)
|
||||||
|
- [ ] Integration tests pass | 集成测试通过
|
||||||
|
|
||||||
### Documentation
|
### Documentation | 文档
|
||||||
|
|
||||||
- [ ] I have updated the documentation accordingly
|
- [ ] I have updated the documentation accordingly | 我已相应更新文档
|
||||||
- [ ] I have updated the README if needed
|
- [ ] I have updated the README if needed | 我已更新 README(如需要)
|
||||||
- [ ] I have added inline code comments where necessary
|
- [ ] I have added inline code comments where necessary | 我已在必要处添加代码注释
|
||||||
- [ ] I have updated type definitions (for TypeScript changes)
|
- [ ] I have updated type definitions (for TypeScript changes) | 我已更新类型定义(TypeScript 变更)
|
||||||
|
- [ ] I have updated API documentation (if applicable) | 我已更新 API 文档(如适用)
|
||||||
|
|
||||||
### Git
|
### Git
|
||||||
|
|
||||||
- [ ] My commits follow the conventional commits format (`feat:`, `fix:`, etc.)
|
- [ ] My commits follow the conventional commits format | 我的提交遵循 Conventional Commits 格式 (`feat:`, `fix:`, etc.)
|
||||||
- [ ] I have rebased my branch on the latest `dev` branch
|
- [ ] I have rebased my branch on the latest `dev` branch | 我已将分支 rebase 到最新的 `dev` 分支
|
||||||
- [ ] There are no merge conflicts
|
- [ ] There are no merge conflicts | 没有合并冲突
|
||||||
|
- [ ] Commit messages are clear and descriptive | 提交信息清晰明确
|
||||||
|
|
||||||
## 🔒 Security Considerations
|
---
|
||||||
|
|
||||||
|
## 🔒 Security Considerations | 安全考虑
|
||||||
|
|
||||||
<!-- Answer these questions for security-sensitive changes -->
|
<!-- Answer these questions for security-sensitive changes -->
|
||||||
|
<!-- 对于安全敏感的变更,请回答以下问题 -->
|
||||||
|
|
||||||
- [ ] No API keys or secrets are hardcoded
|
- [ ] No API keys or secrets are hardcoded | 没有硬编码 API 密钥或密钥
|
||||||
- [ ] User inputs are properly validated
|
- [ ] User inputs are properly validated | 用户输入已正确验证
|
||||||
- [ ] No SQL injection vulnerabilities introduced
|
- [ ] No SQL injection vulnerabilities introduced | 未引入 SQL 注入漏洞
|
||||||
- [ ] Authentication/authorization properly handled
|
- [ ] No XSS vulnerabilities introduced | 未引入 XSS 漏洞
|
||||||
- [ ] N/A (not security-related)
|
- [ ] Authentication/authorization properly handled | 认证/授权已正确处理
|
||||||
|
- [ ] Sensitive data is encrypted | 敏感数据已加密
|
||||||
|
- [ ] N/A (not security-related) | 不适用(非安全相关)
|
||||||
|
|
||||||
## ⚡ Performance Impact
|
---
|
||||||
|
|
||||||
|
## ⚡ Performance Impact | 性能影响
|
||||||
|
|
||||||
<!-- Describe any performance implications -->
|
<!-- Describe any performance implications -->
|
||||||
|
<!-- 描述任何性能影响 -->
|
||||||
|
|
||||||
- [ ] No significant performance impact
|
- [ ] No significant performance impact | 无显著性能影响
|
||||||
- [ ] Performance improved
|
- [ ] Performance improved | 性能提升
|
||||||
- [ ] Performance may be impacted (explain below)
|
- [ ] Performance may be impacted (explain below) | 性能可能受影响(请在下方说明)
|
||||||
|
|
||||||
<!-- If performance impacted, explain: -->
|
<!-- If performance impacted, explain: -->
|
||||||
|
<!-- 如果性能受影响,请说明: -->
|
||||||
|
|
||||||
## 📚 Additional Notes
|
**English:**
|
||||||
|
|
||||||
|
**中文:**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Internationalization | 国际化
|
||||||
|
|
||||||
|
<!-- For UI/documentation changes -->
|
||||||
|
<!-- 对于 UI/文档变更 -->
|
||||||
|
|
||||||
|
- [ ] All user-facing text supports i18n | 所有面向用户的文本支持国际化
|
||||||
|
- [ ] Both English and Chinese versions provided | 提供了中英文版本
|
||||||
|
- [ ] N/A | 不适用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Additional Notes | 补充说明
|
||||||
|
|
||||||
<!-- Any additional information for reviewers -->
|
<!-- Any additional information for reviewers -->
|
||||||
|
<!-- 给审查者的任何补充信息 -->
|
||||||
|
|
||||||
|
**English:**
|
||||||
|
|
||||||
|
**中文:**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## For Bounty Claims
|
## 💰 For Bounty Claims | 赏金申请
|
||||||
|
|
||||||
<!-- Fill this section only if claiming a bounty -->
|
<!-- Fill this section only if claiming a bounty -->
|
||||||
|
<!-- 仅在申请赏金时填写此部分 -->
|
||||||
|
|
||||||
- [ ] This PR is for bounty issue #
|
- [ ] This PR is for bounty issue # | 此 PR 用于赏金 issue #
|
||||||
- [ ] All acceptance criteria from the bounty issue are met
|
- [ ] All acceptance criteria from the bounty issue are met | 满足赏金 issue 的所有验收标准
|
||||||
- [ ] I have included a demo video/screenshots
|
- [ ] I have included a demo video/screenshots | 我已包含演示视频/截图
|
||||||
- [ ] I am ready for payment upon merge
|
- [ ] I am ready for payment upon merge | 我准备好在合并后接收付款
|
||||||
|
|
||||||
**Payment Details:** <!-- Discuss privately with maintainers -->
|
**Payment Details | 付款详情:** <!-- Discuss privately with maintainers | 与维护者私下讨论 -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🙏 Reviewer Notes
|
## 🙏 Reviewer Notes | 审查者注意事项
|
||||||
|
|
||||||
<!-- Optional: anything specific you want reviewers to focus on? -->
|
<!-- Optional: anything specific you want reviewers to focus on? -->
|
||||||
|
<!-- 可选:你希望审查者关注的特定内容? -->
|
||||||
|
|
||||||
|
**English:**
|
||||||
|
|
||||||
|
**中文:**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 PR Size Estimate | PR 大小估计
|
||||||
|
|
||||||
|
<!-- This helps reviewers plan their time -->
|
||||||
|
<!-- 这有助于审查者安排时间 -->
|
||||||
|
|
||||||
|
- [ ] 🟢 Small (< 100 lines) | 小(< 100 行)
|
||||||
|
- [ ] 🟡 Medium (100-500 lines) | 中(100-500 行)
|
||||||
|
- [ ] 🔴 Large (> 500 lines) | 大(> 500 行)
|
||||||
|
|
||||||
|
<!-- For large PRs, consider: -->
|
||||||
|
<!-- 对于大型 PR,考虑: -->
|
||||||
|
<!-- - Breaking into smaller, focused PRs | 拆分为更小、更专注的 PR -->
|
||||||
|
<!-- - Providing a detailed explanation | 提供详细说明 -->
|
||||||
|
<!-- - Highlighting the most important changes | 突出最重要的变更 -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Review Focus Areas | 审查重点
|
||||||
|
|
||||||
|
<!-- Help reviewers know where to focus their attention -->
|
||||||
|
<!-- 帮助审查者了解重点关注的地方 -->
|
||||||
|
|
||||||
|
Please pay special attention to:
|
||||||
|
请特别注意:
|
||||||
|
|
||||||
|
- [ ] Logic changes | 逻辑变更
|
||||||
|
- [ ] Security implications | 安全影响
|
||||||
|
- [ ] Performance optimization | 性能优化
|
||||||
|
- [ ] API changes | API 变更
|
||||||
|
- [ ] Database schema changes | 数据库架构变更
|
||||||
|
- [ ] UI/UX changes | UI/UX 变更
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**By submitting this PR, I confirm that:**
|
**By submitting this PR, I confirm that:**
|
||||||
- [ ] I have read the [Contributing Guidelines](../CONTRIBUTING.md)
|
**提交此 PR,我确认:**
|
||||||
- [ ] I agree to the [Code of Conduct](../CODE_OF_CONDUCT.md)
|
|
||||||
- [ ] My contribution is licensed under the MIT License
|
- [ ] I have read the [Contributing Guidelines](../CONTRIBUTING.md) | 我已阅读[贡献指南](../CONTRIBUTING.md)
|
||||||
|
- [ ] I agree to the [Code of Conduct](../CODE_OF_CONDUCT.md) | 我同意[行为准则](../CODE_OF_CONDUCT.md)
|
||||||
|
- [ ] My contribution is licensed under the MIT License | 我的贡献遵循 MIT 许可证
|
||||||
|
- [ ] I understand this is a voluntary contribution | 我理解这是自愿贡献
|
||||||
|
- [ ] I have the right to submit this code | 我有权提交此代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
🌟 感谢你的贡献!Thank you for your contribution!
|
||||||
|
|
||||||
|
贡献者来自世界各地,我们重视每一份贡献。
|
||||||
|
Contributors come from all around the world, and we value every contribution.
|
||||||
|
|
||||||
|
如果你是首次贡献,欢迎加入我们的社区!
|
||||||
|
If this is your first contribution, welcome to our community!
|
||||||
|
|
||||||
|
💬 需要帮助?Feel free to ask questions in:
|
||||||
|
- GitHub Discussions
|
||||||
|
- Discord: [链接 Link]
|
||||||
|
- Telegram: [链接 Link]
|
||||||
|
-->
|
||||||
|
|||||||
176
.github/workflows/README.md
vendored
Normal file
176
.github/workflows/README.md
vendored
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# GitHub Actions Workflows
|
||||||
|
|
||||||
|
This directory contains the GitHub Actions workflows for the NOFX project.
|
||||||
|
|
||||||
|
## 📚 Documentation Index
|
||||||
|
|
||||||
|
- **[README.md](./README.md)** - This file, overview of all workflows
|
||||||
|
- **[PERMISSIONS.md](./PERMISSIONS.md)** - Detailed permission analysis and security model
|
||||||
|
- **[TRIGGERS.md](./TRIGGERS.md)** - Comparison of event triggers (pull_request vs pull_request_target vs workflow_run)
|
||||||
|
- **[FORK_PR_FLOW.md](./FORK_PR_FLOW.md)** - Complete analysis of what happens when a fork PR is submitted
|
||||||
|
- **[FLOW_DIAGRAM.md](./FLOW_DIAGRAM.md)** - Visual flow diagrams and quick reference
|
||||||
|
- **[SECRETS_SCANNING.md](./SECRETS_SCANNING.md)** - Secrets scanning solutions and TruffleHog setup
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
**Want to understand how fork PRs work?** → Read [FLOW_DIAGRAM.md](./FLOW_DIAGRAM.md)
|
||||||
|
|
||||||
|
**Need security details?** → Read [PERMISSIONS.md](./PERMISSIONS.md)
|
||||||
|
|
||||||
|
**Confused about triggers?** → Read [TRIGGERS.md](./TRIGGERS.md)
|
||||||
|
|
||||||
|
## PR Check Workflows
|
||||||
|
|
||||||
|
We use a **two-workflow pattern** to safely handle PR checks from both internal and fork PRs:
|
||||||
|
|
||||||
|
### 1. `pr-checks-run.yml` - Execute Checks
|
||||||
|
|
||||||
|
**Trigger:** On pull request (opened, synchronize, reopened)
|
||||||
|
|
||||||
|
**Permissions:** Read-only
|
||||||
|
|
||||||
|
**Purpose:** Executes all PR checks with read-only permissions, making it safe for fork PRs.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Checks PR title format (Conventional Commits)
|
||||||
|
- ✅ Calculates PR size
|
||||||
|
- ✅ Runs backend checks (Go formatting, vet, tests)
|
||||||
|
- ✅ Runs frontend checks (linting, type checking, build)
|
||||||
|
- ✅ Saves all results as artifacts
|
||||||
|
|
||||||
|
**Security:** Safe for fork PRs because it only has read permissions and cannot access secrets or modify the repository.
|
||||||
|
|
||||||
|
### 2. `pr-checks-comment.yml` - Post Results
|
||||||
|
|
||||||
|
**Trigger:** When `pr-checks-run.yml` completes (workflow_run)
|
||||||
|
|
||||||
|
**Permissions:** Write (pull-requests, issues)
|
||||||
|
|
||||||
|
**Purpose:** Posts check results as PR comments, running in the main repository context.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Downloads artifacts from `pr-checks-run.yml`
|
||||||
|
- ✅ Reads check results
|
||||||
|
- ✅ Posts a comprehensive comment to the PR
|
||||||
|
|
||||||
|
**Security:** Safe because:
|
||||||
|
- Runs in the main repository context (not fork context)
|
||||||
|
- Has write permissions but doesn't execute untrusted code
|
||||||
|
- Only reads pre-generated results from artifacts
|
||||||
|
|
||||||
|
### 3. `pr-checks.yml` - Strict Checks
|
||||||
|
|
||||||
|
**Trigger:** On pull request
|
||||||
|
|
||||||
|
**Permissions:** Read + conditional write
|
||||||
|
|
||||||
|
**Purpose:** Runs mandatory checks that must pass before PR can be merged.
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- ✅ Validates PR title (blocks merge if invalid)
|
||||||
|
- ✅ Auto-labels PR based on size and files changed (non-fork only)
|
||||||
|
- ✅ Runs backend tests (Go)
|
||||||
|
- ✅ Runs frontend tests (React/TypeScript)
|
||||||
|
- ✅ Security scanning (Trivy, Gitleaks)
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- Fork PRs: Only runs read-only operations (tests, security scans)
|
||||||
|
- Non-fork PRs: Can add labels and comments
|
||||||
|
- Uses `continue-on-error` for operations that may fail on forks
|
||||||
|
|
||||||
|
## Why Two Workflows for PR Checks?
|
||||||
|
|
||||||
|
### The Problem
|
||||||
|
|
||||||
|
When a PR comes from a forked repository:
|
||||||
|
- GitHub restricts `GITHUB_TOKEN` permissions for security
|
||||||
|
- Fork PRs cannot write comments, add labels, or access secrets
|
||||||
|
- This prevents malicious contributors from:
|
||||||
|
- Stealing repository secrets
|
||||||
|
- Modifying workflow files to execute malicious code
|
||||||
|
- Spamming issues/PRs with automated comments
|
||||||
|
|
||||||
|
### The Solution
|
||||||
|
|
||||||
|
**Two-Workflow Pattern:**
|
||||||
|
|
||||||
|
```
|
||||||
|
Fork PR Submitted
|
||||||
|
↓
|
||||||
|
[pr-checks-run.yml]
|
||||||
|
- Runs with read-only permissions
|
||||||
|
- Executes all checks safely
|
||||||
|
- Saves results to artifacts
|
||||||
|
↓
|
||||||
|
[pr-checks-comment.yml]
|
||||||
|
- Triggered by workflow_run
|
||||||
|
- Runs in main repo context (has write permissions)
|
||||||
|
- Downloads artifacts
|
||||||
|
- Posts comment with results
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach:
|
||||||
|
- ✅ Allows fork PRs to run checks
|
||||||
|
- ✅ Safely posts results as comments
|
||||||
|
- ✅ Prevents security vulnerabilities
|
||||||
|
- ✅ Follows GitHub's best practices
|
||||||
|
|
||||||
|
### Can workflow_run Comment on Fork PRs?
|
||||||
|
|
||||||
|
**Yes! ✅ The permissions are sufficient.**
|
||||||
|
|
||||||
|
**Key Understanding:**
|
||||||
|
- `workflow_run` executes in the **base repository** context
|
||||||
|
- Fork PRs exist in the **base repository** (not in the fork)
|
||||||
|
- The base repository's `GITHUB_TOKEN` has write permissions
|
||||||
|
- Therefore, `workflow_run` can comment on fork PRs
|
||||||
|
|
||||||
|
**Security:**
|
||||||
|
- Fork PR code runs in isolated environment (read-only)
|
||||||
|
- Comment workflow doesn't execute fork code
|
||||||
|
- Only reads pre-generated artifact data
|
||||||
|
|
||||||
|
**For detailed permission analysis, see:** [PERMISSIONS.md](./PERMISSIONS.md)
|
||||||
|
|
||||||
|
## Workflow Comparison
|
||||||
|
|
||||||
|
| Workflow | Fork PRs | Write Access | Blocks Merge | Purpose |
|
||||||
|
|----------|----------|--------------|--------------|---------|
|
||||||
|
| `pr-checks-run.yml` | ✅ Yes | ❌ No | ❌ No | Advisory checks |
|
||||||
|
| `pr-checks-comment.yml` | ✅ Yes | ✅ Yes* | ❌ No | Post results |
|
||||||
|
| `pr-checks.yml` | ✅ Yes | ⚠️ Partial | ✅ Yes | Mandatory checks |
|
||||||
|
|
||||||
|
\* Write access only in main repo context, not available to fork PR code
|
||||||
|
|
||||||
|
## File History
|
||||||
|
|
||||||
|
- `pr-checks-advisory.yml.old` - Old advisory workflow that failed on fork PRs (deprecated)
|
||||||
|
- Now replaced by the two-workflow pattern (`pr-checks-run.yml` + `pr-checks-comment.yml`)
|
||||||
|
|
||||||
|
## Testing the Workflows
|
||||||
|
|
||||||
|
### Test with a Fork PR
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Make changes in your fork
|
||||||
|
3. Create a PR to the main repository
|
||||||
|
4. Observe:
|
||||||
|
- `pr-checks-run.yml` runs successfully with read-only access
|
||||||
|
- `pr-checks-comment.yml` posts results as a comment
|
||||||
|
- `pr-checks.yml` runs tests but skips labeling
|
||||||
|
|
||||||
|
### Test with a Branch PR
|
||||||
|
|
||||||
|
1. Create a branch in the main repository
|
||||||
|
2. Make changes
|
||||||
|
3. Create a PR
|
||||||
|
4. Observe:
|
||||||
|
- All workflows run with full permissions
|
||||||
|
- Labels are added automatically
|
||||||
|
- Comments are posted
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [GitHub Actions: Keeping your GitHub Actions and workflows secure Part 1](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/)
|
||||||
|
- [Safely posting comments from untrusted workflows](https://securitylab.github.com/research/github-actions-building-blocks/)
|
||||||
|
- [GitHub Actions: workflow_run trigger](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run)
|
||||||
@@ -8,12 +8,16 @@ on:
|
|||||||
# These checks are advisory only - they won't block PR merging
|
# These checks are advisory only - they won't block PR merging
|
||||||
# Results will be posted as comments to help contributors improve their PRs
|
# Results will be posted as comments to help contributors improve their PRs
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
checks: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pr-info:
|
pr-info:
|
||||||
name: PR Information
|
name: PR Information
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check PR title format
|
- name: Check PR title format
|
||||||
id: check-title
|
id: check-title
|
||||||
@@ -98,8 +102,6 @@ jobs:
|
|||||||
backend-checks:
|
backend-checks:
|
||||||
name: Backend Checks (Advisory)
|
name: Backend Checks (Advisory)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -208,8 +210,6 @@ jobs:
|
|||||||
frontend-checks:
|
frontend-checks:
|
||||||
name: Frontend Checks (Advisory)
|
name: Frontend Checks (Advisory)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
248
.github/workflows/pr-checks-comment.yml
vendored
Normal file
248
.github/workflows/pr-checks-comment.yml
vendored
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
name: PR Checks - Comment
|
||||||
|
|
||||||
|
# This workflow posts ADVISORY check results as comments
|
||||||
|
# Runs in the main repo context with write permissions (SAFE)
|
||||||
|
# Triggered after pr-checks-run.yml completes
|
||||||
|
#
|
||||||
|
# NOTE: PR title and size checks are handled by pr-checks.yml (no duplication)
|
||||||
|
# This workflow only posts backend/frontend advisory check results
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["PR Checks - Run"]
|
||||||
|
types: [completed]
|
||||||
|
|
||||||
|
# Write permissions - SAFE because runs in main repo context
|
||||||
|
# This token has write access to the base repository
|
||||||
|
# Fork PRs exist in the base repo, so we can comment on them
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
actions: read # Needed to download artifacts
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment:
|
||||||
|
name: Post Advisory Check Results
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run if the workflow was triggered by a pull_request event
|
||||||
|
if: github.event.workflow_run.event == 'pull_request'
|
||||||
|
steps:
|
||||||
|
- name: Download artifacts
|
||||||
|
id: download-artifacts
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run-id: ${{ github.event.workflow_run.id }}
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Debug workflow run info
|
||||||
|
run: |
|
||||||
|
echo "=== Workflow Run Debug Info ==="
|
||||||
|
echo "Workflow Run ID: ${{ github.event.workflow_run.id }}"
|
||||||
|
echo "Workflow Run Event: ${{ github.event.workflow_run.event }}"
|
||||||
|
echo "Workflow Run Conclusion: ${{ github.event.workflow_run.conclusion }}"
|
||||||
|
echo "Workflow Run Head SHA: ${{ github.event.workflow_run.head_sha }}"
|
||||||
|
|
||||||
|
- name: List downloaded artifacts
|
||||||
|
run: |
|
||||||
|
echo "=== Checking downloaded artifacts ==="
|
||||||
|
ls -la artifacts/ || echo "⚠️ No artifacts directory found"
|
||||||
|
find artifacts/ -type f || echo "⚠️ No files found in artifacts"
|
||||||
|
echo ""
|
||||||
|
echo "Artifact download result: ${{ steps.download-artifacts.outcome }}"
|
||||||
|
|
||||||
|
- name: Read backend results
|
||||||
|
id: backend
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if [ -f artifacts/backend-results/backend-results.json ]; then
|
||||||
|
echo "=== Backend Results JSON ==="
|
||||||
|
cat artifacts/backend-results/backend-results.json
|
||||||
|
echo "pr_number=$(jq -r '.pr_number' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "fmt_status=$(jq -r '.fmt_status' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "vet_status=$(jq -r '.vet_status' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
echo "test_status=$(jq -r '.test_status' artifacts/backend-results/backend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Read output files
|
||||||
|
if [ -f artifacts/backend-results/fmt-files.txt ]; then
|
||||||
|
echo "fmt_files<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/backend-results/fmt-files.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -f artifacts/backend-results/vet-output-short.txt ]; then
|
||||||
|
echo "vet_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/backend-results/vet-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
if [ -f artifacts/backend-results/test-output-short.txt ]; then
|
||||||
|
echo "test_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/backend-results/test-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "pr_number=0" >> $GITHUB_OUTPUT
|
||||||
|
echo "⚠️ Backend results artifact not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Read frontend results
|
||||||
|
id: frontend
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if [ -f artifacts/frontend-results/frontend-results.json ]; then
|
||||||
|
echo "=== Frontend Results JSON ==="
|
||||||
|
cat artifacts/frontend-results/frontend-results.json
|
||||||
|
echo "build_status=$(jq -r '.build_status' artifacts/frontend-results/frontend-results.json)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
# Read output files
|
||||||
|
if [ -f artifacts/frontend-results/build-output-short.txt ]; then
|
||||||
|
echo "build_output<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
cat artifacts/frontend-results/build-output-short.txt >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ Frontend results artifact not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Post advisory results comment
|
||||||
|
if: steps.backend.outputs.pr_number != '0'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prNumber = ${{ steps.backend.outputs.pr_number }};
|
||||||
|
|
||||||
|
let comment = '## 🤖 Advisory Check Results\n\n';
|
||||||
|
comment += 'These are **advisory** checks to help improve code quality. They won\'t block your PR from being merged.\n\n';
|
||||||
|
comment += '> **Note:** PR title and size checks are handled by the main workflow and may appear in a separate comment.\n\n';
|
||||||
|
|
||||||
|
// Backend checks
|
||||||
|
const fmtStatus = '${{ steps.backend.outputs.fmt_status }}';
|
||||||
|
const vetStatus = '${{ steps.backend.outputs.vet_status }}';
|
||||||
|
const testStatus = '${{ steps.backend.outputs.test_status }}';
|
||||||
|
|
||||||
|
if (fmtStatus || vetStatus || testStatus) {
|
||||||
|
comment += '\n### 🔧 Backend Checks\n\n';
|
||||||
|
|
||||||
|
if (fmtStatus) {
|
||||||
|
comment += '**Go Formatting:** ' + fmtStatus + '\n';
|
||||||
|
const fmtFiles = `${{ steps.backend.outputs.fmt_files }}`;
|
||||||
|
if (fmtFiles && fmtFiles.trim()) {
|
||||||
|
comment += '<details><summary>Files needing formatting</summary>\n\n```\n' + fmtFiles + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vetStatus) {
|
||||||
|
comment += '**Go Vet:** ' + vetStatus + '\n';
|
||||||
|
const vetOutput = `${{ steps.backend.outputs.vet_output }}`;
|
||||||
|
if (vetOutput && vetOutput.trim()) {
|
||||||
|
comment += '<details><summary>Issues found</summary>\n\n```\n' + vetOutput.substring(0, 1000) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testStatus) {
|
||||||
|
comment += '**Tests:** ' + testStatus + '\n';
|
||||||
|
const testOutput = `${{ steps.backend.outputs.test_output }}`;
|
||||||
|
if (testOutput && testOutput.trim()) {
|
||||||
|
comment += '<details><summary>Test output</summary>\n\n```\n' + testOutput.substring(0, 1000) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comment += '\n**Fix locally:**\n';
|
||||||
|
comment += '```bash\n';
|
||||||
|
comment += 'go fmt ./... # Format code\n';
|
||||||
|
comment += 'go vet ./... # Check for issues\n';
|
||||||
|
comment += 'go test ./... # Run tests\n';
|
||||||
|
comment += '```\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Frontend checks
|
||||||
|
const buildStatus = '${{ steps.frontend.outputs.build_status }}';
|
||||||
|
|
||||||
|
if (buildStatus) {
|
||||||
|
comment += '\n### ⚛️ Frontend Checks\n\n';
|
||||||
|
|
||||||
|
comment += '**Build & Type Check:** ' + buildStatus + '\n';
|
||||||
|
const buildOutput = `${{ steps.frontend.outputs.build_output }}`;
|
||||||
|
if (buildOutput && buildOutput.trim()) {
|
||||||
|
comment += '<details><summary>Build output</summary>\n\n```\n' + buildOutput.substring(0, 1000) + '\n```\n</details>\n\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
comment += '\n**Fix locally:**\n';
|
||||||
|
comment += '```bash\n';
|
||||||
|
comment += 'cd web\n';
|
||||||
|
comment += 'npm run build # Test build (includes type checking)\n';
|
||||||
|
comment += '```\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
comment += '\n---\n\n';
|
||||||
|
comment += '### 📖 Resources\n\n';
|
||||||
|
comment += '- [Contributing Guidelines](https://github.com/tinkle-community/nofx/blob/dev/CONTRIBUTING.md)\n';
|
||||||
|
comment += '- [Migration Guide](https://github.com/tinkle-community/nofx/blob/dev/docs/community/MIGRATION_ANNOUNCEMENT.md)\n\n';
|
||||||
|
comment += '**Questions?** Feel free to ask in the comments! 🙏\n\n';
|
||||||
|
comment += '---\n\n';
|
||||||
|
comment += '*These checks are advisory and won\'t block your PR from being merged. This comment is automatically generated from [pr-checks-run.yml](https://github.com/tinkle-community/nofx/blob/dev/.github/workflows/pr-checks-run.yml).*';
|
||||||
|
|
||||||
|
// Post comment
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: comment
|
||||||
|
});
|
||||||
|
|
||||||
|
- name: Post fallback comment if no results
|
||||||
|
if: steps.backend.outputs.pr_number == '0'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
// Try to get PR number from the workflow_run event
|
||||||
|
const pulls = await github.rest.pulls.list({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
state: 'open',
|
||||||
|
head: `${context.repo.owner}:${{ github.event.workflow_run.head_branch }}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pulls.data.length === 0) {
|
||||||
|
console.log('⚠️ Could not find PR for this workflow run');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prNumber = pulls.data[0].number;
|
||||||
|
|
||||||
|
const comment = [
|
||||||
|
'## ⚠️ Advisory Checks - Results Unavailable',
|
||||||
|
'',
|
||||||
|
'The advisory checks workflow completed, but results could not be retrieved.',
|
||||||
|
'',
|
||||||
|
'### Possible reasons:',
|
||||||
|
'- Artifacts were not uploaded successfully',
|
||||||
|
'- Artifacts expired (retention: 1 day)',
|
||||||
|
'- Permission issues',
|
||||||
|
'',
|
||||||
|
'### What to do:',
|
||||||
|
'1. Check the [PR Checks - Run workflow](${{ github.event.workflow_run.html_url }}) logs',
|
||||||
|
'2. Ensure your code passes local checks:',
|
||||||
|
'```bash',
|
||||||
|
'# Backend',
|
||||||
|
'go fmt ./...',
|
||||||
|
'go vet ./...',
|
||||||
|
'go build',
|
||||||
|
'go test ./...',
|
||||||
|
'',
|
||||||
|
'# Frontend (if applicable)',
|
||||||
|
'cd web',
|
||||||
|
'npm run build',
|
||||||
|
'```',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'',
|
||||||
|
'*This is an automated fallback message. The advisory checks ran but results are not available.*'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
issue_number: prNumber,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: comment
|
||||||
|
});
|
||||||
160
.github/workflows/pr-checks-run.yml
vendored
Normal file
160
.github/workflows/pr-checks-run.yml
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
name: PR Checks - Run
|
||||||
|
|
||||||
|
# This workflow runs advisory PR checks with read-only permissions
|
||||||
|
# Safe for fork PRs - results are saved as artifacts
|
||||||
|
# Companion workflow (pr-checks-comment.yml) will post comments
|
||||||
|
#
|
||||||
|
# NOTE: This workflow provides ADVISORY checks (non-blocking)
|
||||||
|
# Main blocking checks are in pr-checks.yml
|
||||||
|
# PR title and size checks are handled by pr-checks.yml (no duplication)
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
branches: [main, dev]
|
||||||
|
|
||||||
|
# Read-only permissions - safe for fork PRs
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Backend advisory checks
|
||||||
|
# Different from pr-checks.yml: these use continue-on-error and generate reports
|
||||||
|
backend-checks:
|
||||||
|
name: Backend Checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y libta-lib-dev || true
|
||||||
|
go mod download || true
|
||||||
|
|
||||||
|
- name: Check Go formatting
|
||||||
|
id: go-fmt
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
UNFORMATTED=$(gofmt -l . 2>/dev/null || echo "")
|
||||||
|
if [ -n "$UNFORMATTED" ]; then
|
||||||
|
echo "status=⚠️ Needs formatting" >> $GITHUB_OUTPUT
|
||||||
|
echo "$UNFORMATTED" | head -10 > fmt-files.txt
|
||||||
|
else
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
echo "" > fmt-files.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run go vet
|
||||||
|
id: go-vet
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if go vet ./... 2>&1 | tee vet-output.txt; then
|
||||||
|
echo "status=✅ Good" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Issues found" >> $GITHUB_OUTPUT
|
||||||
|
cat vet-output.txt | head -20 > vet-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
id: go-test
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
if go test ./... -v 2>&1 | tee test-output.txt; then
|
||||||
|
echo "status=✅ Passed" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Failed" >> $GITHUB_OUTPUT
|
||||||
|
cat test-output.txt | tail -30 > test-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Save backend results
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
cat > backend-results.json <<EOF
|
||||||
|
{
|
||||||
|
"pr_number": ${{ github.event.pull_request.number }},
|
||||||
|
"fmt_status": "${{ steps.go-fmt.outputs.status }}",
|
||||||
|
"vet_status": "${{ steps.go-vet.outputs.status }}",
|
||||||
|
"test_status": "${{ steps.go-test.outputs.status }}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload backend results
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: backend-results
|
||||||
|
path: |
|
||||||
|
backend-results.json
|
||||||
|
fmt-files.txt
|
||||||
|
vet-output-short.txt
|
||||||
|
test-output-short.txt
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# Frontend advisory checks
|
||||||
|
# Different from pr-checks.yml: these use continue-on-error and generate reports
|
||||||
|
frontend-checks:
|
||||||
|
name: Frontend Checks
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '18'
|
||||||
|
|
||||||
|
- name: Check if web directory exists
|
||||||
|
id: check-web
|
||||||
|
run: |
|
||||||
|
if [ -d "web" ]; then
|
||||||
|
echo "exists=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.check-web.outputs.exists == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
continue-on-error: true
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build and Type Check
|
||||||
|
if: steps.check-web.outputs.exists == 'true'
|
||||||
|
id: build
|
||||||
|
working-directory: ./web
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
# build script includes: tsc && vite build
|
||||||
|
if npm run build 2>&1 | tee build-output.txt; then
|
||||||
|
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "status=⚠️ Failed" >> $GITHUB_OUTPUT
|
||||||
|
cat build-output.txt | tail -30 > build-output-short.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Save frontend results
|
||||||
|
if: always() && steps.check-web.outputs.exists == 'true'
|
||||||
|
working-directory: ./web
|
||||||
|
run: |
|
||||||
|
cat > frontend-results.json <<EOF
|
||||||
|
{
|
||||||
|
"pr_number": ${{ github.event.pull_request.number }},
|
||||||
|
"build_status": "${{ steps.build.outputs.status }}"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Upload frontend results
|
||||||
|
if: always() && steps.check-web.outputs.exists == 'true'
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: frontend-results
|
||||||
|
path: |
|
||||||
|
web/frontend-results.json
|
||||||
|
web/build-output-short.txt
|
||||||
|
retention-days: 1
|
||||||
173
.github/workflows/pr-checks.yml
vendored
173
.github/workflows/pr-checks.yml
vendored
@@ -7,13 +7,24 @@ on:
|
|||||||
- dev
|
- dev
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
# Default permissions for all jobs
|
||||||
|
# Note: Fork PRs won't have write access for security
|
||||||
|
# Advisory checks use separate workflow (pr-checks-run.yml + pr-checks-comment.yml)
|
||||||
|
permissions:
|
||||||
|
contents: read # Read repository contents
|
||||||
|
pull-requests: write # Manage PRs (labels, comments) - only works for non-fork PRs
|
||||||
|
issues: write # Manage issues (PRs are issues) - only works for non-fork PRs
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Validate PR title and description
|
# Validate PR title and description
|
||||||
validate-pr:
|
validate-pr:
|
||||||
name: Validate PR Format
|
name: Validate PR Format
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Inherits workflow-level permissions (contents: read, pull-requests: write, issues: write)
|
||||||
steps:
|
steps:
|
||||||
- name: Check PR title format
|
- name: Check PR title format
|
||||||
|
id: semantic-pr
|
||||||
|
continue-on-error: true # Don't block PR if title format is invalid
|
||||||
uses: amannn/action-semantic-pull-request@v5
|
uses: amannn/action-semantic-pull-request@v5
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -29,6 +40,7 @@ jobs:
|
|||||||
chore
|
chore
|
||||||
ci
|
ci
|
||||||
security
|
security
|
||||||
|
build
|
||||||
scopes: |
|
scopes: |
|
||||||
exchange
|
exchange
|
||||||
trader
|
trader
|
||||||
@@ -39,10 +51,70 @@ jobs:
|
|||||||
backend
|
backend
|
||||||
security
|
security
|
||||||
deps
|
deps
|
||||||
|
workflow
|
||||||
|
github
|
||||||
|
actions
|
||||||
|
config
|
||||||
|
docker
|
||||||
|
build
|
||||||
|
release
|
||||||
requireScope: false
|
requireScope: false
|
||||||
|
|
||||||
|
- name: Comment on invalid PR title
|
||||||
|
if: steps.semantic-pr.outcome == 'failure'
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
continue-on-error: true # Don't fail for fork PRs
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const prTitle = context.payload.pull_request.title;
|
||||||
|
const isFork = context.payload.pull_request.head.repo.full_name !== context.payload.pull_request.base.repo.full_name;
|
||||||
|
|
||||||
|
const comment = [
|
||||||
|
'## ⚠️ PR Title Format Suggestion',
|
||||||
|
'',
|
||||||
|
"Your PR title doesn't follow the Conventional Commits format, but **this won't block your PR from being merged**.",
|
||||||
|
'',
|
||||||
|
`**Current title:** \`${prTitle}\``,
|
||||||
|
'',
|
||||||
|
'**Recommended format:** `type(scope): description`',
|
||||||
|
'',
|
||||||
|
'### Valid types:',
|
||||||
|
'`feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `security`, `build`',
|
||||||
|
'',
|
||||||
|
'### Common scopes (optional):',
|
||||||
|
'`exchange`, `trader`, `ai`, `api`, `ui`, `frontend`, `backend`, `security`, `deps`, `workflow`, `github`, `actions`, `config`, `docker`, `build`, `release`',
|
||||||
|
'',
|
||||||
|
'### Examples:',
|
||||||
|
'- `feat(trader): add new trading strategy`',
|
||||||
|
'- `fix(api): resolve authentication issue`',
|
||||||
|
'- `docs: update README`',
|
||||||
|
'- `chore(deps): update dependencies`',
|
||||||
|
'- `ci(workflow): improve GitHub Actions`',
|
||||||
|
'',
|
||||||
|
'**Note:** This is a suggestion to improve consistency. Your PR can still be reviewed and merged.',
|
||||||
|
'',
|
||||||
|
'---',
|
||||||
|
'*This is an automated comment. You can update the PR title anytime.*'
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
if (!isFork) {
|
||||||
|
try {
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.payload.pull_request.number,
|
||||||
|
body: comment
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not post comment (expected for fork PRs):', error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Fork PR - comment will be posted by pr-checks-comment.yml');
|
||||||
|
}
|
||||||
|
|
||||||
- name: Check PR size
|
- name: Check PR size
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
|
continue-on-error: true # Don't fail for fork PRs
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const pr = context.payload.pull_request;
|
const pr = context.payload.pull_request;
|
||||||
@@ -50,6 +122,9 @@ jobs:
|
|||||||
const deletions = pr.deletions;
|
const deletions = pr.deletions;
|
||||||
const total = additions + deletions;
|
const total = additions + deletions;
|
||||||
|
|
||||||
|
// Check if this is a fork PR
|
||||||
|
const isFork = pr.head.repo.full_name !== pr.base.repo.full_name;
|
||||||
|
|
||||||
let label = '';
|
let label = '';
|
||||||
let comment = '';
|
let comment = '';
|
||||||
|
|
||||||
@@ -64,6 +139,9 @@ jobs:
|
|||||||
comment = '🚨 This PR is **large** (>' + total + ' lines changed). Please consider breaking it into smaller, focused PRs for easier review.';
|
comment = '🚨 This PR is **large** (>' + total + ' lines changed). Please consider breaking it into smaller, focused PRs for easier review.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add labels/comments for non-fork PRs (fork PRs don't have write permission)
|
||||||
|
if (!isFork) {
|
||||||
|
try {
|
||||||
// Add size label
|
// Add size label
|
||||||
await github.rest.issues.addLabels({
|
await github.rest.issues.addLabels({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
@@ -81,11 +159,19 @@ jobs:
|
|||||||
body: comment
|
body: comment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to add label/comment (expected for fork PRs):', error.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Fork PR detected - skipping label/comment (will be handled by pr-checks-comment.yml)');
|
||||||
|
}
|
||||||
|
|
||||||
# Backend tests
|
# Backend checks (simplified - no TA-Lib required)
|
||||||
backend-tests:
|
backend-checks:
|
||||||
name: Backend Tests (Go)
|
name: Backend Code Quality (Go)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read # Only need read access for testing
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -95,11 +181,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
|
||||||
- name: Install TA-Lib
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libta-lib-dev
|
|
||||||
|
|
||||||
- name: Cache Go modules
|
- name: Cache Go modules
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
@@ -112,32 +193,32 @@ jobs:
|
|||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
- name: Run go fmt
|
- name: Run go fmt
|
||||||
|
continue-on-error: true # Don't block PR if formatting issues found
|
||||||
run: |
|
run: |
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
echo "Please run 'go fmt' on your code"
|
echo "⚠️ Code formatting issues found. Please run 'go fmt ./...' locally."
|
||||||
|
echo ""
|
||||||
|
echo "Files needing formatting:"
|
||||||
gofmt -s -l .
|
gofmt -s -l .
|
||||||
|
echo ""
|
||||||
|
echo "This is a warning and won't block your PR from being merged."
|
||||||
exit 1
|
exit 1
|
||||||
|
else
|
||||||
|
echo "✅ All Go files are properly formatted"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run go vet
|
- name: Run go vet
|
||||||
run: go vet ./...
|
run: go vet ./...
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: go test -v -race -coverprofile=coverage.out ./...
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: go build -v -o nofx
|
run: go build -v -o nofx
|
||||||
|
|
||||||
- name: Upload coverage
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
file: ./coverage.out
|
|
||||||
flags: backend
|
|
||||||
|
|
||||||
# Frontend tests
|
# Frontend tests
|
||||||
frontend-tests:
|
frontend-tests:
|
||||||
name: Frontend Tests (React/TypeScript)
|
name: Frontend Tests (React/TypeScript)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read # Only need read access for testing
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -159,24 +240,21 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run linter
|
- name: Build and Type Check
|
||||||
working-directory: ./web
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
- name: Run type check
|
|
||||||
working-directory: ./web
|
|
||||||
run: npm run type-check || true # Don't fail on type errors for now
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
# Note: build script runs "tsc && vite build" which includes type checking
|
||||||
|
|
||||||
# Auto-label based on files changed
|
# Auto-label based on files changed
|
||||||
auto-label:
|
auto-label:
|
||||||
name: Auto Label PR
|
name: Auto Label PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Only run for non-fork PRs (fork PRs don't have write permission)
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
permissions:
|
permissions:
|
||||||
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
issues: write # Required: PRs are issues, labeler needs to modify issue labels
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@v5
|
- uses: actions/labeler@v5
|
||||||
with:
|
with:
|
||||||
@@ -187,6 +265,9 @@ jobs:
|
|||||||
security-check:
|
security-check:
|
||||||
name: Security Scan
|
name: Security Scan
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
security-events: write # Required: Upload SARIF results to GitHub Security
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -209,29 +290,51 @@ jobs:
|
|||||||
secrets-check:
|
secrets-check:
|
||||||
name: Check for Secrets
|
name: Check for Secrets
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read # Only need read access for scanning
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Run Gitleaks
|
- name: Run TruffleHog OSS
|
||||||
uses: gitleaks/gitleaks-action@v2
|
uses: trufflesecurity/trufflehog@main
|
||||||
env:
|
with:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
path: ./
|
||||||
|
base: ${{ github.event.pull_request.base.sha }}
|
||||||
|
head: ${{ github.event.pull_request.head.sha }}
|
||||||
|
extra_args: --debug --only-verified
|
||||||
|
|
||||||
# All checks passed
|
# All checks passed
|
||||||
all-checks:
|
all-checks:
|
||||||
name: All Checks Passed
|
name: All Checks Passed
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [validate-pr, backend-tests, frontend-tests, security-check, secrets-check]
|
needs: [validate-pr, backend-checks, frontend-tests, security-check, secrets-check]
|
||||||
if: always()
|
if: always()
|
||||||
|
permissions:
|
||||||
|
contents: read # Only need read access for status checking
|
||||||
steps:
|
steps:
|
||||||
- name: Check all jobs
|
- name: Check all jobs
|
||||||
run: |
|
run: |
|
||||||
if [ "${{ contains(needs.*.result, 'failure') }}" == "true" ]; then
|
# Note: validate-pr uses continue-on-error, so it won't block even if title format is invalid
|
||||||
echo "Some checks failed"
|
# We only care about actual test failures
|
||||||
|
echo "validate-pr: ${{ needs.validate-pr.result }}"
|
||||||
|
echo "backend-checks: ${{ needs.backend-checks.result }}"
|
||||||
|
echo "frontend-tests: ${{ needs.frontend-tests.result }}"
|
||||||
|
echo "security-check: ${{ needs.security-check.result }}"
|
||||||
|
echo "secrets-check: ${{ needs.secrets-check.result }}"
|
||||||
|
|
||||||
|
# Check if any critical checks failed (excluding validate-pr which is advisory)
|
||||||
|
if [[ "${{ needs.backend-checks.result }}" == "failure" ]] || \
|
||||||
|
[[ "${{ needs.frontend-tests.result }}" == "failure" ]] || \
|
||||||
|
[[ "${{ needs.security-check.result }}" == "failure" ]] || \
|
||||||
|
[[ "${{ needs.secrets-check.result }}" == "failure" ]]; then
|
||||||
|
echo "❌ Critical checks failed"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "All checks passed!"
|
echo "✅ All critical checks passed!"
|
||||||
|
if [[ "${{ needs.validate-pr.result }}" != "success" ]]; then
|
||||||
|
echo "ℹ️ Note: PR title format check is advisory only and doesn't block merging"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -124,11 +124,12 @@ A Binance-compatible decentralized perpetual futures exchange!
|
|||||||
- 🌐 **Multi-chain support** - trade on your preferred EVM chain
|
- 🌐 **Multi-chain support** - trade on your preferred EVM chain
|
||||||
|
|
||||||
**Quick Start:**
|
**Quick Start:**
|
||||||
1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
1. Register via [Aster Referral Link](https://www.asterdex.com/en/referral/fdfc0e) (get fee discounts!)
|
||||||
2. Connect your main wallet and create an API wallet
|
2. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||||
3. Copy the API Signer address and Private Key
|
3. Connect your main wallet and create an API wallet
|
||||||
4. ~~Set `"exchange": "aster"` in config.json~~ *Configure through web interface*
|
4. Copy the API Signer address and Private Key
|
||||||
5. Add `"aster_user"`, `"aster_signer"`, and `"aster_private_key"`
|
5. Set `"exchange": "aster"` in config.json
|
||||||
|
6. Add `"aster_user"`, `"aster_signer"`, and `"aster_private_key"`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -406,7 +407,7 @@ Before configuring the system, you need to obtain AI API keys. Choose one of the
|
|||||||
|
|
||||||
**How to get Qwen API Key:**
|
**How to get Qwen API Key:**
|
||||||
|
|
||||||
1. **Visit**: [https://dashscope.aliyuncs.com](https://dashscope.aliyuncs.com)
|
1. **Visit**: [https://dashscope.console.aliyun.com](https://dashscope.console.aliyun.com)
|
||||||
2. **Register**: Sign up with Alibaba Cloud account
|
2. **Register**: Sign up with Alibaba Cloud account
|
||||||
3. **Enable Service**: Activate DashScope service
|
3. **Enable Service**: Activate DashScope service
|
||||||
4. **Create API Key**:
|
4. **Create API Key**:
|
||||||
@@ -543,12 +544,13 @@ Open your browser and visit: **🌐 http://localhost:3000**
|
|||||||
- 🌐 Multi-chain support (ETH, BSC, Polygon)
|
- 🌐 Multi-chain support (ETH, BSC, Polygon)
|
||||||
- 🌍 No KYC required
|
- 🌍 No KYC required
|
||||||
|
|
||||||
**Step 1**: Create Aster API Wallet
|
**Step 1**: Register and Create Aster API Wallet
|
||||||
|
|
||||||
1. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
1. Register via [Aster Referral Link](https://www.asterdex.com/en/referral/fdfc0e) (get fee discounts!)
|
||||||
2. Connect your main wallet (MetaMask, WalletConnect, etc.)
|
2. Visit [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||||
3. Click "Create API Wallet"
|
3. Connect your main wallet (MetaMask, WalletConnect, etc.)
|
||||||
4. **Save these 3 items immediately:**
|
4. Click "Create API Wallet"
|
||||||
|
5. **Save these 3 items immediately:**
|
||||||
- Main Wallet address (User)
|
- Main Wallet address (User)
|
||||||
- API Wallet address (Signer)
|
- API Wallet address (Signer)
|
||||||
- API Wallet Private Key (⚠️ shown only once!)
|
- API Wallet Private Key (⚠️ shown only once!)
|
||||||
@@ -565,9 +567,9 @@ Open your browser and visit: **🌐 http://localhost:3000**
|
|||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
"aster_user": "0xYOUR_MAIN_WALLET_ADDRESS_HERE",
|
||||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
"aster_signer": "0xYOUR_API_WALLET_SIGNER_ADDRESS_HERE",
|
||||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
"aster_private_key": "your_api_wallet_private_key_without_0x_prefix",
|
||||||
|
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -1168,6 +1170,8 @@ GET /api/health # Health check
|
|||||||
|
|
||||||
## 🛠️ Common Issues
|
## 🛠️ Common Issues
|
||||||
|
|
||||||
|
> 📖 **For detailed troubleshooting:** See the comprehensive [Troubleshooting Guide](docs/guides/TROUBLESHOOTING.md) ([中文版](docs/guides/TROUBLESHOOTING.zh-CN.md))
|
||||||
|
|
||||||
### 1. Compilation error: TA-Lib not found
|
### 1. Compilation error: TA-Lib not found
|
||||||
|
|
||||||
**Solution**: Install TA-Lib library
|
**Solution**: Install TA-Lib library
|
||||||
@@ -1271,7 +1275,7 @@ We welcome contributions from the community! See our comprehensive guides:
|
|||||||
|
|
||||||
- [Binance API](https://binance-docs.github.io/apidocs/futures/en/) - Binance Futures API
|
- [Binance API](https://binance-docs.github.io/apidocs/futures/en/) - Binance Futures API
|
||||||
- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API
|
- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API
|
||||||
- [Qwen](https://dashscope.aliyuncs.com/) - Alibaba Cloud Qwen
|
- [Qwen](https://dashscope.console.aliyun.com/) - Alibaba Cloud Qwen
|
||||||
- [TA-Lib](https://ta-lib.org/) - Technical indicator library
|
- [TA-Lib](https://ta-lib.org/) - Technical indicator library
|
||||||
- [Recharts](https://recharts.org/) - React chart library
|
- [Recharts](https://recharts.org/) - React chart library
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/base32"
|
"encoding/base32"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"nofx/market"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -185,6 +188,7 @@ func (d *Database) createTables() error {
|
|||||||
`ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔
|
`ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔
|
||||||
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源
|
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源
|
||||||
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源
|
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源
|
||||||
|
`ALTER TABLE traders ADD COLUMN use_inside_coins BOOLEAN DEFAULT 0`, // 是否使用内置AI评分信号源
|
||||||
`ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`, // 系统提示词模板名称
|
`ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`, // 系统提示词模板名称
|
||||||
`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址
|
`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址
|
||||||
`ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称
|
`ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称
|
||||||
@@ -412,6 +416,7 @@ type TraderRecord struct {
|
|||||||
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
|
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
|
||||||
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
|
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
|
||||||
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
|
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
|
||||||
|
UseInsideCoins bool `json:"use_inside_coins"` // 是否使用内置评分信号源
|
||||||
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
||||||
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
||||||
SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称
|
SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称
|
||||||
@@ -767,9 +772,9 @@ func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, ap
|
|||||||
// CreateTrader 创建交易员
|
// CreateTrader 创建交易员
|
||||||
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
||||||
_, err := d.db.Exec(`
|
_, err := d.db.Exec(`
|
||||||
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin)
|
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, use_inside_coins, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?, ?, ?, ?, ?, ?)
|
||||||
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin)
|
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.UseInsideCoins, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -779,7 +784,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
|||||||
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
|
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
|
||||||
COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
|
COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
|
||||||
COALESCE(trading_symbols, '') as trading_symbols,
|
COALESCE(trading_symbols, '') as trading_symbols,
|
||||||
COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,
|
COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,COALESCE(use_inside_coins, 0) as use_inside_coins,
|
||||||
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
|
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
|
||||||
COALESCE(system_prompt_template, 'default') as system_prompt_template,
|
COALESCE(system_prompt_template, 'default') as system_prompt_template,
|
||||||
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
||||||
@@ -797,7 +802,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
|||||||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||||||
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
||||||
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
|
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
|
||||||
&trader.UseCoinPool, &trader.UseOITop,
|
&trader.UseCoinPool, &trader.UseOITop, &trader.UseInsideCoins,
|
||||||
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
|
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
|
||||||
&trader.IsCrossMargin,
|
&trader.IsCrossMargin,
|
||||||
&trader.CreatedAt, &trader.UpdatedAt,
|
&trader.CreatedAt, &trader.UpdatedAt,
|
||||||
@@ -853,12 +858,7 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
|||||||
|
|
||||||
err := d.db.QueryRow(`
|
err := d.db.QueryRow(`
|
||||||
SELECT
|
SELECT
|
||||||
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running,
|
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at,
|
||||||
COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(t.altcoin_leverage, 5) as altcoin_leverage,
|
|
||||||
COALESCE(t.trading_symbols, '') as trading_symbols, COALESCE(t.use_coin_pool, 0) as use_coin_pool,
|
|
||||||
COALESCE(t.use_oi_top, 0) as use_oi_top, COALESCE(t.custom_prompt, '') as custom_prompt,
|
|
||||||
COALESCE(t.override_base_prompt, 0) as override_base_prompt, COALESCE(t.is_cross_margin, 1) as is_cross_margin,
|
|
||||||
t.created_at, t.updated_at,
|
|
||||||
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at,
|
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at,
|
||||||
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
|
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
|
||||||
COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
||||||
@@ -873,8 +873,6 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
|||||||
`, traderID, userID).Scan(
|
`, traderID, userID).Scan(
|
||||||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||||||
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
||||||
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols, &trader.UseCoinPool,
|
|
||||||
&trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.IsCrossMargin,
|
|
||||||
&trader.CreatedAt, &trader.UpdatedAt,
|
&trader.CreatedAt, &trader.UpdatedAt,
|
||||||
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
|
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
|
||||||
&aiModel.CreatedAt, &aiModel.UpdatedAt,
|
&aiModel.CreatedAt, &aiModel.UpdatedAt,
|
||||||
@@ -940,6 +938,35 @@ func (d *Database) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCustomCoins 获取所有交易员自定义币种 / Get all trader-customized currencies
|
||||||
|
func (d *Database) GetCustomCoins() []string {
|
||||||
|
var symbol string
|
||||||
|
var symbols []string
|
||||||
|
_ = d.db.QueryRow(`
|
||||||
|
SELECT GROUP_CONCAT(custom_coins , ',') as symbol
|
||||||
|
FROM main.traders where custom_coins != ''
|
||||||
|
`).Scan(&symbol)
|
||||||
|
// 检测用户是否未配置币种 - 兼容性
|
||||||
|
if symbol == "" {
|
||||||
|
symbolJSON, _ := d.GetSystemConfig("default_coins")
|
||||||
|
if err := json.Unmarshal([]byte(symbolJSON), &symbols); err != nil {
|
||||||
|
log.Printf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err)
|
||||||
|
symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// filter Symbol
|
||||||
|
for _, s := range strings.Split(symbol, ",") {
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
coin := market.Normalize(s)
|
||||||
|
if !slices.Contains(symbols, coin) {
|
||||||
|
symbols = append(symbols, coin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return symbols
|
||||||
|
}
|
||||||
|
|
||||||
// Close 关闭数据库连接
|
// Close 关闭数据库连接
|
||||||
func (d *Database) Close() error {
|
func (d *Database) Close() error {
|
||||||
return d.db.Close()
|
return d.db.Close()
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ func GetFullDecisionWithCustomPrompt(ctx *Context, mcpClient *mcp.Client, custom
|
|||||||
// 4. 解析AI响应
|
// 4. 解析AI响应
|
||||||
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
|
decision, err := parseFullDecisionResponse(aiResponse, ctx.Account.TotalEquity, ctx.BTCETHLeverage, ctx.AltcoinLeverage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("解析AI响应失败: %w", err)
|
return decision, fmt.Errorf("解析AI响应失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
decision.Timestamp = time.Now()
|
decision.Timestamp = time.Now()
|
||||||
@@ -409,7 +409,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthL
|
|||||||
return &FullDecision{
|
return &FullDecision{
|
||||||
CoTTrace: cotTrace,
|
CoTTrace: cotTrace,
|
||||||
Decisions: []Decision{},
|
Decisions: []Decision{},
|
||||||
}, fmt.Errorf("提取决策失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
|
}, fmt.Errorf("提取决策失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 验证决策
|
// 3. 验证决策
|
||||||
@@ -417,7 +417,7 @@ func parseFullDecisionResponse(aiResponse string, accountEquity float64, btcEthL
|
|||||||
return &FullDecision{
|
return &FullDecision{
|
||||||
CoTTrace: cotTrace,
|
CoTTrace: cotTrace,
|
||||||
Decisions: decisions,
|
Decisions: decisions,
|
||||||
}, fmt.Errorf("决策验证失败: %w\n\n=== AI思维链分析 ===\n%s", err, cotTrace)
|
}, fmt.Errorf("决策验证失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &FullDecision{
|
return &FullDecision{
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ COPY go.mod go.sum ./
|
|||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN CGO_ENABLED=1 GOOS=linux go build -trimpath -ldflags="-s -w" -o nofx .
|
RUN CGO_ENABLED=1 GOOS=linux \
|
||||||
|
CGO_CFLAGS="-D_LARGEFILE64_SOURCE" \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o nofx .
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────
|
||||||
# Runtime Stage (Minimal Executable Environment)
|
# Runtime Stage (Minimal Executable Environment)
|
||||||
|
|||||||
@@ -51,7 +51,12 @@ nofx/
|
|||||||
│
|
│
|
||||||
├── market/ # Market data fetching
|
├── market/ # Market data fetching
|
||||||
│ └── data.go # Market data & technical indicators (TA-Lib)
|
│ └── data.go # Market data & technical indicators (TA-Lib)
|
||||||
│
|
│ └── api_client.go # Market data acquisition API
|
||||||
|
│ └── websocket_client.go # Market data acquisition WebSocket interface
|
||||||
|
│ └── combined_streams.go # Market data acquisition: Combined streaming (single link to subscribe to multiple cryptocurrencies)
|
||||||
|
│ └── monitor.go # Market data cache
|
||||||
|
│ └── types.go # market structure
|
||||||
|
|
||||||
├── pool/ # Coin pool management
|
├── pool/ # Coin pool management
|
||||||
│ └── coin_pool.go # AI500 + OI Top merged pool
|
│ └── coin_pool.go # AI500 + OI Top merged pool
|
||||||
│
|
│
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ nofx/
|
|||||||
│
|
│
|
||||||
├── market/ # 市场数据获取
|
├── market/ # 市场数据获取
|
||||||
│ └── data.go # 市场数据与技术指标(TA-Lib)
|
│ └── data.go # 市场数据与技术指标(TA-Lib)
|
||||||
|
│ └── api_client.go # 行情获取 Api接口
|
||||||
|
│ └── websocket_client.go # 行情获取 Websocket接口
|
||||||
|
│ └── combined_streams.go # 行情获取 组合流式(单链接订阅多个币种)
|
||||||
|
│ └── monitor.go # 行情数据缓存
|
||||||
|
│ └── types.go # market结构体
|
||||||
│
|
│
|
||||||
├── pool/ # 币种池管理
|
├── pool/ # 币种池管理
|
||||||
│ └── coin_pool.go # AI500 + OI Top 合并池
|
│ └── coin_pool.go # AI500 + OI Top 合并池
|
||||||
|
|||||||
612
docs/guides/TROUBLESHOOTING.md
Normal file
612
docs/guides/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# 🔧 Troubleshooting Guide
|
||||||
|
|
||||||
|
This guide helps you diagnose and fix common issues before submitting a bug report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Quick Diagnostic Checklist
|
||||||
|
|
||||||
|
Before reporting a bug, please check:
|
||||||
|
|
||||||
|
1. ✅ **Backend is running**: `docker compose ps` or `ps aux | grep nofx`
|
||||||
|
2. ✅ **Frontend is accessible**: Open http://localhost:3000 in browser
|
||||||
|
3. ✅ **API is responding**: `curl http://localhost:8080/api/health`
|
||||||
|
4. ✅ **Check logs for errors**: See [How to Capture Logs](#how-to-capture-logs) below
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Common Issues & Solutions
|
||||||
|
|
||||||
|
### 1. Trading Issues
|
||||||
|
|
||||||
|
#### ❌ Only Opening Short Positions (Issue #202)
|
||||||
|
|
||||||
|
**Symptom:** AI only opens short positions, never long positions, even when market is bullish.
|
||||||
|
|
||||||
|
**Root Cause:** Binance account is in **One-way Mode** instead of **Hedge Mode**.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Login to [Binance Futures](https://www.binance.com/futures/BTCUSDT)
|
||||||
|
2. Click **⚙️ Preferences** (top right)
|
||||||
|
3. Select **Position Mode**
|
||||||
|
4. Switch to **Hedge Mode** (双向持仓)
|
||||||
|
5. ⚠️ **Important:** Close all positions before switching
|
||||||
|
|
||||||
|
**Why this happens:**
|
||||||
|
- Code uses `PositionSide(LONG)` and `PositionSide(SHORT)` parameters
|
||||||
|
- These only work in Hedge Mode
|
||||||
|
- In One-way Mode, orders fail or only one direction works
|
||||||
|
|
||||||
|
**For Subaccounts:**
|
||||||
|
- Some Binance subaccounts may not have permission to change position mode
|
||||||
|
- Use main account or contact Binance support to enable this permission
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Order Error: `code=-4061` Position Side Mismatch
|
||||||
|
|
||||||
|
**Error Message:** `Order's position side does not match user's setting`
|
||||||
|
|
||||||
|
**Solution:** Same as above - switch to Hedge Mode.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Leverage Error: `Subaccounts restricted to 5x leverage`
|
||||||
|
|
||||||
|
**Symptom:** Orders fail with leverage error when trying to use >5x leverage.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
1. Open Web UI → Trader Settings
|
||||||
|
2. Set leverage to 5x or lower:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. Or use main account (supports up to 50x BTC/ETH, 20x altcoins)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Positions Not Executing
|
||||||
|
|
||||||
|
**Check these:**
|
||||||
|
1. **API Permissions**:
|
||||||
|
- Go to Binance → API Management
|
||||||
|
- Verify "Enable Futures" is checked
|
||||||
|
- Check IP whitelist (if enabled)
|
||||||
|
|
||||||
|
2. **Account Balance**:
|
||||||
|
- Ensure sufficient USDT in Futures wallet
|
||||||
|
- Check margin usage is not at 100%
|
||||||
|
|
||||||
|
3. **Symbol Status**:
|
||||||
|
- Verify trading pair is active on exchange
|
||||||
|
- Check if symbol is in maintenance mode
|
||||||
|
|
||||||
|
4. **Decision Logs**:
|
||||||
|
```bash
|
||||||
|
# Check latest decision
|
||||||
|
ls -lt decision_logs/your_trader_id/ | head -5
|
||||||
|
cat decision_logs/your_trader_id/latest_file.json
|
||||||
|
```
|
||||||
|
- Look for AI decision: was it "wait", "hold", or actual trade?
|
||||||
|
- Check if position_size_usd is within limits
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. AI Decision Issues
|
||||||
|
|
||||||
|
#### ❌ AI Always Says "Wait" / "Hold"
|
||||||
|
|
||||||
|
**Possible Causes:**
|
||||||
|
1. **Market Conditions**: AI may genuinely see no good opportunities
|
||||||
|
2. **Risk Limits**: Account equity too low, margin usage too high
|
||||||
|
3. **Historical Performance**: AI being cautious after losses
|
||||||
|
|
||||||
|
**How to Check:**
|
||||||
|
```bash
|
||||||
|
# View latest decision reasoning
|
||||||
|
cat decision_logs/your_trader_id/$(ls -t decision_logs/your_trader_id/ | head -1)
|
||||||
|
```
|
||||||
|
|
||||||
|
Look at the AI's Chain-of-Thought reasoning section.
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Wait for better market conditions
|
||||||
|
- Check if all candidate coins have low liquidity
|
||||||
|
- Verify `use_default_coins: true` or coin pool API is working
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ AI Making Bad Decisions
|
||||||
|
|
||||||
|
**Remember:** AI trading is experimental and not guaranteed to be profitable.
|
||||||
|
|
||||||
|
**Things to Check:**
|
||||||
|
1. **Decision Interval**: Is it too short? (Recommended: 3-5 minutes)
|
||||||
|
2. **Leverage Settings**: Too aggressive?
|
||||||
|
3. **Historical Feedback**: Check performance logs to see if AI is learning
|
||||||
|
4. **Market Volatility**: High volatility = higher risk
|
||||||
|
|
||||||
|
**Adjustments:**
|
||||||
|
- Reduce leverage for more conservative trading
|
||||||
|
- Increase decision interval to reduce over-trading
|
||||||
|
- Use smaller initial balance for testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Connection & API Issues
|
||||||
|
|
||||||
|
#### ❌ Docker Image Pull Failed (China Mainland)
|
||||||
|
|
||||||
|
**Error:** `ERROR [internal] load metadata for docker.io/library/...`
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- `docker compose build` or `docker compose up` hangs
|
||||||
|
- Timeout errors: `timeout`, `connection refused`
|
||||||
|
- Cannot pull images from Docker Hub
|
||||||
|
|
||||||
|
**Root Cause:**
|
||||||
|
Access to Docker Hub is restricted or extremely slow in mainland China.
|
||||||
|
|
||||||
|
**Solution 1: Configure Docker Registry Mirror (Recommended)**
|
||||||
|
|
||||||
|
1. **Edit Docker configuration file:**
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
sudo nano /etc/docker/daemon.json
|
||||||
|
|
||||||
|
# macOS (Docker Desktop)
|
||||||
|
# Settings → Docker Engine
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add China registry mirrors:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://docker.m.daocloud.io",
|
||||||
|
"https://docker.1panel.live",
|
||||||
|
"https://hub.rat.dev",
|
||||||
|
"https://dockerpull.com",
|
||||||
|
"https://dockerhub.icu"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Restart Docker:**
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
sudo systemctl restart docker
|
||||||
|
|
||||||
|
# macOS/Windows
|
||||||
|
# Restart Docker Desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Rebuild:**
|
||||||
|
```bash
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2: Use VPN**
|
||||||
|
|
||||||
|
1. Connect to VPN (Taiwan nodes recommended)
|
||||||
|
2. Ensure **global mode** instead of rule-based mode
|
||||||
|
3. Re-run `docker compose build`
|
||||||
|
|
||||||
|
**Solution 3: Offline Image Download**
|
||||||
|
|
||||||
|
If above methods don't work:
|
||||||
|
|
||||||
|
1. **Use image proxy websites:**
|
||||||
|
- https://proxy.vvvv.ee/images.html (offline download available)
|
||||||
|
- https://github.com/dongyubin/DockerHub (mirror list)
|
||||||
|
|
||||||
|
2. **Manually import images:**
|
||||||
|
```bash
|
||||||
|
# After downloading image files
|
||||||
|
docker load -i golang-1.25-alpine.tar
|
||||||
|
docker load -i node-20-alpine.tar
|
||||||
|
docker load -i nginx-alpine.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Verify images are loaded:**
|
||||||
|
```bash
|
||||||
|
docker images | grep golang
|
||||||
|
docker images | grep node
|
||||||
|
docker images | grep nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify registry mirror is working:**
|
||||||
|
```bash
|
||||||
|
# Check Docker info
|
||||||
|
docker info | grep -A 10 "Registry Mirrors"
|
||||||
|
|
||||||
|
# Should show your configured mirrors
|
||||||
|
```
|
||||||
|
|
||||||
|
**Related Issue:** [#168](https://github.com/tinkle-community/nofx/issues/168)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Backend Won't Start
|
||||||
|
|
||||||
|
**Error:** `port 8080 already in use`
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Find what's using the port
|
||||||
|
lsof -i :8080
|
||||||
|
# OR
|
||||||
|
netstat -tulpn | grep 8080
|
||||||
|
|
||||||
|
# Kill the process or change port in .env
|
||||||
|
NOFX_BACKEND_PORT=8081
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Frontend Can't Connect to Backend
|
||||||
|
|
||||||
|
**Symptoms:**
|
||||||
|
- UI shows "Loading..." forever
|
||||||
|
- Browser console shows 404 or network errors
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. **Check backend is running:**
|
||||||
|
```bash
|
||||||
|
docker compose ps # Should show backend as "Up"
|
||||||
|
# OR
|
||||||
|
curl http://localhost:8080/api/health # Should return {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check port configuration:**
|
||||||
|
- Backend default: 8080
|
||||||
|
- Frontend default: 3000
|
||||||
|
- Verify `.env` settings match
|
||||||
|
|
||||||
|
3. **CORS Issues:**
|
||||||
|
- If running frontend and backend on different ports/domains
|
||||||
|
- Check browser console for CORS errors
|
||||||
|
- Backend should allow frontend origin
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Exchange API Errors
|
||||||
|
|
||||||
|
**Common Errors:**
|
||||||
|
- `code=-1021, msg=Timestamp for this request is outside of the recvWindow`
|
||||||
|
- `invalid signature`
|
||||||
|
- `timestamp` errors
|
||||||
|
|
||||||
|
**Root Cause:**
|
||||||
|
System time is inaccurate, differing from Binance server time by more than allowed range (typically 5 seconds).
|
||||||
|
|
||||||
|
**Solution 1: Sync System Time (Recommended)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Method 1: Use ntpdate (most common)
|
||||||
|
sudo ntpdate pool.ntp.org
|
||||||
|
|
||||||
|
# Method 2: Use other NTP servers
|
||||||
|
sudo ntpdate -s time.nist.gov
|
||||||
|
sudo ntpdate -s ntp.aliyun.com # Aliyun NTP (fast in China)
|
||||||
|
|
||||||
|
# Method 3: Enable automatic time sync (Linux)
|
||||||
|
sudo timedatectl set-ntp true
|
||||||
|
|
||||||
|
# Verify time is correct
|
||||||
|
date
|
||||||
|
# Should show current accurate time
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker Environment Special Note:**
|
||||||
|
|
||||||
|
If using Docker, container time may be out of sync with host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container time
|
||||||
|
docker exec nofx-backend date
|
||||||
|
|
||||||
|
# If time is wrong, restart Docker service
|
||||||
|
sudo systemctl restart docker
|
||||||
|
|
||||||
|
# Or add timezone in docker-compose.yml
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai # or your timezone
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution 2: Verify API Keys**
|
||||||
|
|
||||||
|
If errors persist after time sync:
|
||||||
|
|
||||||
|
1. **Check API Keys:**
|
||||||
|
- Not expired
|
||||||
|
- Have correct permissions (Futures enabled)
|
||||||
|
- IP whitelist includes your server IP
|
||||||
|
|
||||||
|
2. **Regenerate API Keys:**
|
||||||
|
- Login to Binance → API Management
|
||||||
|
- Delete old key
|
||||||
|
- Create new key
|
||||||
|
- Update NOFX configuration
|
||||||
|
|
||||||
|
**Solution 3: Check Rate Limits**
|
||||||
|
|
||||||
|
Binance has strict API rate limits:
|
||||||
|
|
||||||
|
- **Requests per minute limit**
|
||||||
|
- Reduce number of traders
|
||||||
|
- Increase decision interval (e.g., from 1min to 3-5min)
|
||||||
|
|
||||||
|
**Related Issue:** [#60](https://github.com/tinkle-community/nofx/issues/60)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Frontend Issues
|
||||||
|
|
||||||
|
#### ❌ UI Not Updating / Showing Old Data
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
1. **Hard Refresh:**
|
||||||
|
- Chrome/Firefox: `Ctrl+Shift+R` (Windows/Linux) or `Cmd+Shift+R` (Mac)
|
||||||
|
- Safari: `Cmd+Option+R`
|
||||||
|
|
||||||
|
2. **Clear Browser Cache:**
|
||||||
|
- Settings → Privacy → Clear browsing data
|
||||||
|
- Or open in Incognito/Private mode
|
||||||
|
|
||||||
|
3. **Check SWR Polling:**
|
||||||
|
- Frontend uses SWR with 5-10s intervals
|
||||||
|
- Data should auto-refresh
|
||||||
|
- Check browser console for fetch errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Charts Not Rendering
|
||||||
|
|
||||||
|
**Possible Causes:**
|
||||||
|
1. No historical data yet (system just started)
|
||||||
|
2. JavaScript errors in console
|
||||||
|
3. Browser compatibility issues
|
||||||
|
|
||||||
|
**Solutions:**
|
||||||
|
- Wait 5-10 minutes for data to accumulate
|
||||||
|
- Check browser console (F12) for errors
|
||||||
|
- Try different browser (Chrome recommended)
|
||||||
|
- Ensure backend API endpoints are returning data
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Database Issues
|
||||||
|
|
||||||
|
#### ❌ `database is locked` Error
|
||||||
|
|
||||||
|
**Cause:** SQLite database being accessed by multiple processes.
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
```bash
|
||||||
|
# Stop all NOFX processes
|
||||||
|
docker compose down
|
||||||
|
# OR
|
||||||
|
pkill nofx
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose up -d
|
||||||
|
# OR
|
||||||
|
./nofx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ Trader Configuration Not Saving
|
||||||
|
|
||||||
|
**Check:**
|
||||||
|
1. **Permissions:**
|
||||||
|
```bash
|
||||||
|
ls -l config.db trading.db
|
||||||
|
# Should be writable by current user
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Disk Space:**
|
||||||
|
```bash
|
||||||
|
df -h # Ensure disk not full
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Database Integrity:**
|
||||||
|
```bash
|
||||||
|
sqlite3 config.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 How to Capture Logs
|
||||||
|
|
||||||
|
### Backend Logs
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
```bash
|
||||||
|
# View last 100 lines
|
||||||
|
docker compose logs backend --tail=100
|
||||||
|
|
||||||
|
# Follow live logs
|
||||||
|
docker compose logs -f backend
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
docker compose logs backend --tail=500 > backend_logs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manual/PM2:**
|
||||||
|
```bash
|
||||||
|
# Terminal where you ran ./nofx shows logs
|
||||||
|
|
||||||
|
# PM2:
|
||||||
|
pm2 logs nofx --lines 100
|
||||||
|
|
||||||
|
# Save to file
|
||||||
|
pm2 logs nofx --lines 500 > backend_logs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend Logs (Browser Console)
|
||||||
|
|
||||||
|
1. **Open DevTools:**
|
||||||
|
- Press `F12` or Right-click → Inspect
|
||||||
|
|
||||||
|
2. **Console Tab:**
|
||||||
|
- See JavaScript errors and warnings
|
||||||
|
- Look for red error messages
|
||||||
|
|
||||||
|
3. **Network Tab:**
|
||||||
|
- Filter by "XHR" or "Fetch"
|
||||||
|
- Look for failed requests (red status codes)
|
||||||
|
- Click on failed request → Preview/Response to see error details
|
||||||
|
|
||||||
|
4. **Capture Screenshot:**
|
||||||
|
- Windows: `Win+Shift+S`
|
||||||
|
- Mac: `Cmd+Shift+4`
|
||||||
|
- Or use browser DevTools screenshot feature
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Decision Logs (Trading Issues)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent decision logs
|
||||||
|
ls -lt decision_logs/your_trader_id/ | head -10
|
||||||
|
|
||||||
|
# View latest decision
|
||||||
|
cat decision_logs/your_trader_id/$(ls -t decision_logs/your_trader_id/ | head -1) | jq .
|
||||||
|
|
||||||
|
# Search for specific symbol
|
||||||
|
grep -r "BTCUSDT" decision_logs/your_trader_id/
|
||||||
|
|
||||||
|
# Find decisions that resulted in trades
|
||||||
|
grep -r '"action": "open_' decision_logs/your_trader_id/
|
||||||
|
```
|
||||||
|
|
||||||
|
**What to look for in decision logs:**
|
||||||
|
- `chain_of_thought`: AI's reasoning process
|
||||||
|
- `user_prompt`: Market data AI received
|
||||||
|
- `decision`: Final decision (action, symbol, leverage, etc.)
|
||||||
|
- `account_state`: Account balance, margin, positions at decision time
|
||||||
|
- `execution_result`: Whether trade succeeded or failed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Diagnostic Commands
|
||||||
|
|
||||||
|
### System Health Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend health
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
|
||||||
|
# List all traders
|
||||||
|
curl http://localhost:8080/api/traders
|
||||||
|
|
||||||
|
# Check specific trader status
|
||||||
|
curl http://localhost:8080/api/status?trader_id=your_trader_id
|
||||||
|
|
||||||
|
# Get account info
|
||||||
|
curl http://localhost:8080/api/account?trader_id=your_trader_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check all containers
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# Check resource usage
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# Restart specific service
|
||||||
|
docker compose restart backend
|
||||||
|
docker compose restart frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Queries
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check traders in database
|
||||||
|
sqlite3 config.db "SELECT id, name, ai_model_id, exchange_id, is_running FROM traders;"
|
||||||
|
|
||||||
|
# Check AI models
|
||||||
|
sqlite3 config.db "SELECT id, name, model_type, enabled FROM ai_models;"
|
||||||
|
|
||||||
|
# Check system config
|
||||||
|
sqlite3 config.db "SELECT key, value FROM system_config;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Still Having Issues?
|
||||||
|
|
||||||
|
If you've tried all the above and still have problems:
|
||||||
|
|
||||||
|
1. **Gather Information:**
|
||||||
|
- Backend logs (last 100 lines)
|
||||||
|
- Frontend console screenshot
|
||||||
|
- Decision logs (if trading issue)
|
||||||
|
- Your environment details
|
||||||
|
|
||||||
|
2. **Submit Bug Report:**
|
||||||
|
- Use the [Bug Report Template](../../.github/ISSUE_TEMPLATE/bug_report.md)
|
||||||
|
- Include all logs and screenshots
|
||||||
|
- Describe what you've already tried
|
||||||
|
|
||||||
|
3. **Join Community:**
|
||||||
|
- [Telegram Developer Community](https://t.me/nofx_dev_community)
|
||||||
|
- [GitHub Discussions](https://github.com/tinkle-community/nofx/discussions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 Emergency: System Completely Broken
|
||||||
|
|
||||||
|
**Complete Reset (⚠️ Will lose trading history):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stop everything
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# Backup databases (just in case)
|
||||||
|
cp config.db config.db.backup
|
||||||
|
cp trading.db trading.db.backup
|
||||||
|
|
||||||
|
# Remove databases (fresh start)
|
||||||
|
rm config.db trading.db
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# Reconfigure through web UI
|
||||||
|
open http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Partial Reset (Keep configuration, clear logs):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Clear decision logs
|
||||||
|
rm -rf decision_logs/*
|
||||||
|
|
||||||
|
# Clear Docker cache and rebuild
|
||||||
|
docker compose down
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 Additional Resources
|
||||||
|
|
||||||
|
- **[FAQ](faq.en.md)** - Frequently Asked Questions
|
||||||
|
- **[Getting Started](../getting-started/README.md)** - Setup guide
|
||||||
|
- **[Architecture Docs](../architecture/README.md)** - How the system works
|
||||||
|
- **[CLAUDE.md](../../CLAUDE.md)** - Developer documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-11-02
|
||||||
612
docs/guides/TROUBLESHOOTING.zh-CN.md
Normal file
612
docs/guides/TROUBLESHOOTING.zh-CN.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# 🔧 故障排查指南
|
||||||
|
|
||||||
|
本指南帮助您在提交 bug 报告前自行诊断和修复常见问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 快速诊断清单
|
||||||
|
|
||||||
|
提交 bug 前,请检查:
|
||||||
|
|
||||||
|
1. ✅ **后端正在运行**: `docker compose ps` 或 `ps aux | grep nofx`
|
||||||
|
2. ✅ **前端可访问**: 在浏览器打开 http://localhost:3000
|
||||||
|
3. ✅ **API 正常响应**: `curl http://localhost:8080/api/health`
|
||||||
|
4. ✅ **检查日志中的错误**: 参见下方 [如何捕获日志](#如何捕获日志)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 常见问题与解决方案
|
||||||
|
|
||||||
|
### 1. 交易问题
|
||||||
|
|
||||||
|
#### ❌ 只开空单,不开多单 (Issue #202)
|
||||||
|
|
||||||
|
**症状:** AI 只开空仓,从不开多仓,即使市场看涨。
|
||||||
|
|
||||||
|
**根本原因:** 币安账户处于**单向持仓模式**而非**双向持仓模式**。
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. 登录 [币安合约交易](https://www.binance.com/zh-CN/futures/BTCUSDT)
|
||||||
|
2. 点击右上角 **⚙️ 偏好设置**
|
||||||
|
3. 选择 **持仓模式**
|
||||||
|
4. 切换为 **双向持仓** (Hedge Mode)
|
||||||
|
5. ⚠️ **重要:** 切换前必须先平掉所有持仓
|
||||||
|
|
||||||
|
**为什么会这样:**
|
||||||
|
- 代码使用 `PositionSide(LONG)` 和 `PositionSide(SHORT)` 参数
|
||||||
|
- 这些参数只在双向持仓模式下有效
|
||||||
|
- 在单向持仓模式下,订单会失败或只有一个方向有效
|
||||||
|
|
||||||
|
**关于子账户:**
|
||||||
|
- 部分币安子账户可能没有权限更改持仓模式
|
||||||
|
- 使用主账户或联系币安客服开通此权限
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 订单错误: `code=-4061` 持仓方向不匹配
|
||||||
|
|
||||||
|
**错误信息:** `Order's position side does not match user's setting`
|
||||||
|
|
||||||
|
**解决方案:** 同上 - 切换到双向持仓模式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 杠杆错误: `子账户限制最高5倍杠杆`
|
||||||
|
|
||||||
|
**症状:** 尝试使用 >5倍杠杆时订单失败。
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. 打开 Web 界面 → 交易员设置
|
||||||
|
2. 将杠杆设置为 5倍或更低:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"btc_eth_leverage": 5,
|
||||||
|
"altcoin_leverage": 5
|
||||||
|
}
|
||||||
|
```
|
||||||
|
3. 或使用主账户(支持最高 50倍 BTC/ETH,20倍山寨币)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 持仓无法执行
|
||||||
|
|
||||||
|
**检查以下内容:**
|
||||||
|
1. **API 权限**:
|
||||||
|
- 进入币安 → API 管理
|
||||||
|
- 确认"启用合约"已勾选
|
||||||
|
- 检查 IP 白名单(如果启用)
|
||||||
|
|
||||||
|
2. **账户余额**:
|
||||||
|
- 确保合约钱包中有足够的 USDT
|
||||||
|
- 检查保证金使用率未达到 100%
|
||||||
|
|
||||||
|
3. **交易对状态**:
|
||||||
|
- 确认交易对在交易所处于活跃状态
|
||||||
|
- 检查交易对是否处于维护模式
|
||||||
|
|
||||||
|
4. **决策日志**:
|
||||||
|
```bash
|
||||||
|
# 检查最新决策
|
||||||
|
ls -lt decision_logs/your_trader_id/ | head -5
|
||||||
|
cat decision_logs/your_trader_id/latest_file.json
|
||||||
|
```
|
||||||
|
- 查看 AI 决策:是"wait"、"hold"还是实际交易?
|
||||||
|
- 检查 position_size_usd 是否在限制范围内
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. AI 决策问题
|
||||||
|
|
||||||
|
#### ❌ AI 总是说"等待"/"持有"
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
1. **市场情况**: AI 可能确实没看到好的机会
|
||||||
|
2. **风险限制**: 账户净值太低、保证金使用率太高
|
||||||
|
3. **历史表现**: AI 在亏损后变得谨慎
|
||||||
|
|
||||||
|
**如何检查:**
|
||||||
|
```bash
|
||||||
|
# 查看最新决策推理
|
||||||
|
cat decision_logs/your_trader_id/$(ls -t decision_logs/your_trader_id/ | head -1)
|
||||||
|
```
|
||||||
|
|
||||||
|
查看 AI 的思维链(Chain-of-Thought)推理部分。
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 等待更好的市场条件
|
||||||
|
- 检查候选币种是否流动性都太低
|
||||||
|
- 确认 `use_default_coins: true` 或币种池 API 正常工作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ AI 做出错误决策
|
||||||
|
|
||||||
|
**请记住:** AI 交易是实验性的,不保证盈利。
|
||||||
|
|
||||||
|
**需要检查的事项:**
|
||||||
|
1. **决策间隔**: 是否太短?(推荐: 3-5分钟)
|
||||||
|
2. **杠杆设置**: 是否过于激进?
|
||||||
|
3. **历史反馈**: 查看表现日志,看 AI 是否在学习
|
||||||
|
4. **市场波动**: 高波动 = 更高风险
|
||||||
|
|
||||||
|
**调整建议:**
|
||||||
|
- 降低杠杆以实现更保守的交易
|
||||||
|
- 增加决策间隔以减少过度交易
|
||||||
|
- 使用较小的初始余额进行测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 连接和 API 问题
|
||||||
|
|
||||||
|
#### ❌ Docker 镜像下载失败 (中国大陆)
|
||||||
|
|
||||||
|
**错误:** `ERROR [internal] load metadata for docker.io/library/...`
|
||||||
|
|
||||||
|
**症状:**
|
||||||
|
- `docker compose build` 或 `docker compose up` 卡住
|
||||||
|
- 超时错误: `timeout`、`connection refused`
|
||||||
|
- 无法从 Docker Hub 拉取镜像
|
||||||
|
|
||||||
|
**根本原因:**
|
||||||
|
中国大陆访问 Docker Hub 受限或速度极慢。
|
||||||
|
|
||||||
|
**解决方案 1: 配置 Docker 镜像加速器(推荐)**
|
||||||
|
|
||||||
|
1. **编辑 Docker 配置文件:**
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
sudo nano /etc/docker/daemon.json
|
||||||
|
|
||||||
|
# macOS (Docker Desktop)
|
||||||
|
# Settings → Docker Engine
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **添加国内镜像源:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"registry-mirrors": [
|
||||||
|
"https://docker.m.daocloud.io",
|
||||||
|
"https://docker.1panel.live",
|
||||||
|
"https://hub.rat.dev",
|
||||||
|
"https://dockerpull.com",
|
||||||
|
"https://dockerhub.icu"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **重启 Docker:**
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
sudo systemctl restart docker
|
||||||
|
|
||||||
|
# macOS/Windows
|
||||||
|
# 重启 Docker Desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **重新构建:**
|
||||||
|
```bash
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
**解决方案 2: 使用 VPN**
|
||||||
|
|
||||||
|
1. 连接 VPN(推荐台湾节点)
|
||||||
|
2. 确保使用**全局模式**而非规则模式
|
||||||
|
3. 重新运行 `docker compose build`
|
||||||
|
|
||||||
|
**解决方案 3: 离线下载镜像**
|
||||||
|
|
||||||
|
如果上述方法都不行:
|
||||||
|
|
||||||
|
1. **使用镜像代理网站下载:**
|
||||||
|
- https://proxy.vvvv.ee/images.html (可离线下载)
|
||||||
|
- https://github.com/dongyubin/DockerHub (镜像加速列表)
|
||||||
|
|
||||||
|
2. **手动导入镜像:**
|
||||||
|
```bash
|
||||||
|
# 下载镜像文件后
|
||||||
|
docker load -i golang-1.25-alpine.tar
|
||||||
|
docker load -i node-20-alpine.tar
|
||||||
|
docker load -i nginx-alpine.tar
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证镜像已加载:**
|
||||||
|
```bash
|
||||||
|
docker images | grep golang
|
||||||
|
docker images | grep node
|
||||||
|
docker images | grep nginx
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证镜像加速器是否生效:**
|
||||||
|
```bash
|
||||||
|
# 查看 Docker 信息
|
||||||
|
docker info | grep -A 10 "Registry Mirrors"
|
||||||
|
|
||||||
|
# 应该显示你配置的镜像源
|
||||||
|
```
|
||||||
|
|
||||||
|
**相关 Issue:** [#168](https://github.com/tinkle-community/nofx/issues/168)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 后端无法启动
|
||||||
|
|
||||||
|
**错误:** `port 8080 already in use`
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```bash
|
||||||
|
# 查找占用端口的进程
|
||||||
|
lsof -i :8080
|
||||||
|
# 或
|
||||||
|
netstat -tulpn | grep 8080
|
||||||
|
|
||||||
|
# 杀死进程或在 .env 中更改端口
|
||||||
|
NOFX_BACKEND_PORT=8081
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 前端无法连接后端
|
||||||
|
|
||||||
|
**症状:**
|
||||||
|
- UI 显示"加载中..."一直不结束
|
||||||
|
- 浏览器控制台显示 404 或网络错误
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. **检查后端是否运行:**
|
||||||
|
```bash
|
||||||
|
docker compose ps # 应显示 backend 为 "Up"
|
||||||
|
# 或
|
||||||
|
curl http://localhost:8080/api/health # 应返回 {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **检查端口配置:**
|
||||||
|
- 后端默认: 8080
|
||||||
|
- 前端默认: 3000
|
||||||
|
- 确认 `.env` 设置匹配
|
||||||
|
|
||||||
|
3. **CORS 问题:**
|
||||||
|
- 如果前端和后端运行在不同端口/域名
|
||||||
|
- 检查浏览器控制台的 CORS 错误
|
||||||
|
- 后端应允许前端来源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 交易所 API 错误
|
||||||
|
|
||||||
|
**常见错误:**
|
||||||
|
- `code=-1021, msg=Timestamp for this request is outside of the recvWindow`
|
||||||
|
- `invalid signature`
|
||||||
|
- `timestamp` 错误
|
||||||
|
|
||||||
|
**根本原因:**
|
||||||
|
系统时间不准确,与币安服务器时间相差超过允许范围(通常是 5 秒)。
|
||||||
|
|
||||||
|
**解决方案 1: 同步系统时间(推荐)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方法 1: 使用 ntpdate (最常用)
|
||||||
|
sudo ntpdate pool.ntp.org
|
||||||
|
|
||||||
|
# 方法 2: 使用其他 NTP 服务器
|
||||||
|
sudo ntpdate -s time.nist.gov
|
||||||
|
sudo ntpdate -s ntp.aliyun.com # 阿里云 NTP (中国大陆快)
|
||||||
|
|
||||||
|
# 方法 3: 启用自动时间同步 (Linux)
|
||||||
|
sudo timedatectl set-ntp true
|
||||||
|
|
||||||
|
# 验证时间是否正确
|
||||||
|
date
|
||||||
|
# 应该显示正确的当前时间
|
||||||
|
```
|
||||||
|
|
||||||
|
**Docker 环境特别注意:**
|
||||||
|
|
||||||
|
如果使用 Docker,容器时间可能与宿主机不同步:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查容器时间
|
||||||
|
docker exec nofx-backend date
|
||||||
|
|
||||||
|
# 如果时间错误,重启 Docker 服务
|
||||||
|
sudo systemctl restart docker
|
||||||
|
|
||||||
|
# 或在 docker-compose.yml 中添加时区设置
|
||||||
|
environment:
|
||||||
|
- TZ=Asia/Shanghai # 或您的时区
|
||||||
|
```
|
||||||
|
|
||||||
|
**解决方案 2: 验证 API 密钥**
|
||||||
|
|
||||||
|
如果时间同步后仍有错误:
|
||||||
|
|
||||||
|
1. **检查 API 密钥:**
|
||||||
|
- 未过期
|
||||||
|
- 有正确权限(已启用合约)
|
||||||
|
- IP 白名单包含您的服务器 IP
|
||||||
|
|
||||||
|
2. **重新生成 API 密钥:**
|
||||||
|
- 登录币安 → API 管理
|
||||||
|
- 删除旧密钥
|
||||||
|
- 创建新密钥
|
||||||
|
- 更新 NOFX 配置
|
||||||
|
|
||||||
|
**解决方案 3: 检查速率限制**
|
||||||
|
|
||||||
|
币安有严格的 API 速率限制:
|
||||||
|
|
||||||
|
- **每分钟请求数限制**
|
||||||
|
- 减少交易员数量
|
||||||
|
- 增加决策间隔时间(例如从 1 分钟改为 3-5 分钟)
|
||||||
|
|
||||||
|
**相关 Issue:** [#60](https://github.com/tinkle-community/nofx/issues/60)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 前端问题
|
||||||
|
|
||||||
|
#### ❌ UI 不更新 / 显示旧数据
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
1. **强制刷新:**
|
||||||
|
- Chrome/Firefox: `Ctrl+Shift+R` (Windows/Linux) 或 `Cmd+Shift+R` (Mac)
|
||||||
|
- Safari: `Cmd+Option+R`
|
||||||
|
|
||||||
|
2. **清除浏览器缓存:**
|
||||||
|
- 设置 → 隐私 → 清除浏览数据
|
||||||
|
- 或在无痕/隐私模式下打开
|
||||||
|
|
||||||
|
3. **检查 SWR 轮询:**
|
||||||
|
- 前端使用 5-10秒间隔的 SWR
|
||||||
|
- 数据应自动刷新
|
||||||
|
- 检查浏览器控制台是否有 fetch 错误
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 图表不渲染
|
||||||
|
|
||||||
|
**可能原因:**
|
||||||
|
1. 暂无历史数据(系统刚启动)
|
||||||
|
2. 控制台中有 JavaScript 错误
|
||||||
|
3. 浏览器兼容性问题
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
- 等待 5-10 分钟让数据积累
|
||||||
|
- 检查浏览器控制台(F12)是否有错误
|
||||||
|
- 尝试不同浏览器(推荐 Chrome)
|
||||||
|
- 确保后端 API 端点正在返回数据
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 数据库问题
|
||||||
|
|
||||||
|
#### ❌ `database is locked` 错误
|
||||||
|
|
||||||
|
**原因:** SQLite 数据库被多个进程访问。
|
||||||
|
|
||||||
|
**解决方案:**
|
||||||
|
```bash
|
||||||
|
# 停止所有 NOFX 进程
|
||||||
|
docker compose down
|
||||||
|
# 或
|
||||||
|
pkill nofx
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
docker compose up -d
|
||||||
|
# 或
|
||||||
|
./nofx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### ❌ 交易员配置无法保存
|
||||||
|
|
||||||
|
**检查:**
|
||||||
|
1. **权限:**
|
||||||
|
```bash
|
||||||
|
ls -l config.db trading.db
|
||||||
|
# 应该对当前用户可写
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **磁盘空间:**
|
||||||
|
```bash
|
||||||
|
df -h # 确保磁盘未满
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **数据库完整性:**
|
||||||
|
```bash
|
||||||
|
sqlite3 config.db "PRAGMA integrity_check;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 如何捕获日志
|
||||||
|
|
||||||
|
### 后端日志
|
||||||
|
|
||||||
|
**Docker:**
|
||||||
|
```bash
|
||||||
|
# 查看最后 100 行
|
||||||
|
docker compose logs backend --tail=100
|
||||||
|
|
||||||
|
# 实时跟踪日志
|
||||||
|
docker compose logs -f backend
|
||||||
|
|
||||||
|
# 保存到文件
|
||||||
|
docker compose logs backend --tail=500 > backend_logs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
**手动/PM2:**
|
||||||
|
```bash
|
||||||
|
# 运行 ./nofx 的终端会显示日志
|
||||||
|
|
||||||
|
# PM2:
|
||||||
|
pm2 logs nofx --lines 100
|
||||||
|
|
||||||
|
# 保存到文件
|
||||||
|
pm2 logs nofx --lines 500 > backend_logs.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 前端日志(浏览器控制台)
|
||||||
|
|
||||||
|
1. **打开开发者工具:**
|
||||||
|
- 按 `F12` 或右键 → 检查
|
||||||
|
|
||||||
|
2. **Console(控制台)标签:**
|
||||||
|
- 查看 JavaScript 错误和警告
|
||||||
|
- 寻找红色错误消息
|
||||||
|
|
||||||
|
3. **Network(网络)标签:**
|
||||||
|
- 按"XHR"或"Fetch"筛选
|
||||||
|
- 查找失败的请求(红色状态码)
|
||||||
|
- 点击失败的请求 → Preview/Response 查看错误详情
|
||||||
|
|
||||||
|
4. **捕获截图:**
|
||||||
|
- Windows: `Win+Shift+S`
|
||||||
|
- Mac: `Cmd+Shift+4`
|
||||||
|
- 或使用浏览器开发者工具截图功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 决策日志(交易问题)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 列出最近的决策日志
|
||||||
|
ls -lt decision_logs/your_trader_id/ | head -10
|
||||||
|
|
||||||
|
# 查看最新决策
|
||||||
|
cat decision_logs/your_trader_id/$(ls -t decision_logs/your_trader_id/ | head -1) | jq .
|
||||||
|
|
||||||
|
# 搜索特定交易对
|
||||||
|
grep -r "BTCUSDT" decision_logs/your_trader_id/
|
||||||
|
|
||||||
|
# 查找执行交易的决策
|
||||||
|
grep -r '"action": "open_' decision_logs/your_trader_id/
|
||||||
|
```
|
||||||
|
|
||||||
|
**决策日志中要查看的内容:**
|
||||||
|
- `chain_of_thought`: AI 的推理过程
|
||||||
|
- `user_prompt`: AI 收到的市场数据
|
||||||
|
- `decision`: 最终决策(动作、交易对、杠杆等)
|
||||||
|
- `account_state`: 决策时的账户余额、保证金、持仓
|
||||||
|
- `execution_result`: 交易是否成功
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 诊断命令
|
||||||
|
|
||||||
|
### 系统健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 后端健康状态
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
|
||||||
|
# 列出所有交易员
|
||||||
|
curl http://localhost:8080/api/traders
|
||||||
|
|
||||||
|
# 检查特定交易员状态
|
||||||
|
curl http://localhost:8080/api/status?trader_id=your_trader_id
|
||||||
|
|
||||||
|
# 获取账户信息
|
||||||
|
curl http://localhost:8080/api/account?trader_id=your_trader_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查所有容器
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
# 检查资源使用
|
||||||
|
docker stats
|
||||||
|
|
||||||
|
# 重启特定服务
|
||||||
|
docker compose restart backend
|
||||||
|
docker compose restart frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 数据库查询
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查数据库中的交易员
|
||||||
|
sqlite3 config.db "SELECT id, name, ai_model_id, exchange_id, is_running FROM traders;"
|
||||||
|
|
||||||
|
# 检查 AI 模型
|
||||||
|
sqlite3 config.db "SELECT id, name, model_type, enabled FROM ai_models;"
|
||||||
|
|
||||||
|
# 检查系统配置
|
||||||
|
sqlite3 config.db "SELECT key, value FROM system_config;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 仍有问题?
|
||||||
|
|
||||||
|
如果尝试了上述所有方法仍有问题:
|
||||||
|
|
||||||
|
1. **收集信息:**
|
||||||
|
- 后端日志(最后 100 行)
|
||||||
|
- 前端控制台截图
|
||||||
|
- 决策日志(如果是交易问题)
|
||||||
|
- 您的环境详情
|
||||||
|
|
||||||
|
2. **提交 Bug 报告:**
|
||||||
|
- 使用 [Bug 报告模板](../../.github/ISSUE_TEMPLATE/bug_report.md)
|
||||||
|
- 包含所有日志和截图
|
||||||
|
- 描述您已尝试的方法
|
||||||
|
|
||||||
|
3. **加入社区:**
|
||||||
|
- [Telegram 开发者社区](https://t.me/nofx_dev_community)
|
||||||
|
- [GitHub Discussions](https://github.com/tinkle-community/nofx/discussions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆘 紧急情况:系统完全损坏
|
||||||
|
|
||||||
|
**完全重置 (⚠️ 将丢失交易历史):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 停止所有服务
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
# 备份数据库(以防万一)
|
||||||
|
cp config.db config.db.backup
|
||||||
|
cp trading.db trading.db.backup
|
||||||
|
|
||||||
|
# 删除数据库(全新开始)
|
||||||
|
rm config.db trading.db
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
docker compose up -d --build
|
||||||
|
|
||||||
|
# 通过 Web UI 重新配置
|
||||||
|
open http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
**部分重置(保留配置,清除日志):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 清除决策日志
|
||||||
|
rm -rf decision_logs/*
|
||||||
|
|
||||||
|
# 清除 Docker 缓存并重建
|
||||||
|
docker compose down
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 其他资源
|
||||||
|
|
||||||
|
- **[FAQ](faq.zh-CN.md)** - 常见问题
|
||||||
|
- **[快速开始](../getting-started/README.zh-CN.md)** - 安装指南
|
||||||
|
- **[架构文档](../architecture/README.zh-CN.md)** - 系统工作原理
|
||||||
|
- **[CLAUDE.md](../../CLAUDE.md)** - 开发者文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新:** 2025-11-02
|
||||||
@@ -1,25 +1,206 @@
|
|||||||
# Frequently Asked Questions
|
# Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
## Binance Position Mode Error (code=-4061)
|
Quick answers to common questions. For detailed troubleshooting, see [Troubleshooting Guide](TROUBLESHOOTING.md).
|
||||||
|
|
||||||
**Error Message**: `Order's position side does not match user's setting`
|
|
||||||
|
|
||||||
**Cause**: The system requires Hedge Mode (dual position), but your Binance account is set to One-way Mode.
|
|
||||||
|
|
||||||
### Solution
|
|
||||||
|
|
||||||
1. Login to [Binance Futures Trading Platform](https://www.binance.com/en/futures/BTCUSDT)
|
|
||||||
|
|
||||||
2. Click **⚙️ Preferences** in the top right corner
|
|
||||||
|
|
||||||
3. Select **Position Mode**
|
|
||||||
|
|
||||||
4. Switch to **Hedge Mode** (Dual Position)
|
|
||||||
|
|
||||||
5. Confirm the change
|
|
||||||
|
|
||||||
**Note**: You must close all open positions before switching modes.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
For more issues, check [GitHub Issues](https://github.com/tinkle-community/nofx/issues)
|
## General Questions
|
||||||
|
|
||||||
|
### What is NOFX?
|
||||||
|
NOFX is an AI-powered cryptocurrency trading bot that uses large language models (LLMs) to make trading decisions on futures markets.
|
||||||
|
|
||||||
|
### Which exchanges are supported?
|
||||||
|
- ✅ Binance Futures
|
||||||
|
- ✅ Hyperliquid
|
||||||
|
- 🚧 More exchanges coming soon
|
||||||
|
|
||||||
|
### Is NOFX profitable?
|
||||||
|
AI trading is **experimental** and **not guaranteed** to be profitable. Always start with small amounts and never invest more than you can afford to lose.
|
||||||
|
|
||||||
|
### Can I run multiple traders simultaneously?
|
||||||
|
Yes! NOFX supports running multiple traders with different configurations, AI models, and trading strategies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup & Configuration
|
||||||
|
|
||||||
|
### What are the system requirements?
|
||||||
|
- **OS**: Linux, macOS, or Windows (Docker recommended)
|
||||||
|
- **RAM**: 2GB minimum, 4GB recommended
|
||||||
|
- **Disk**: 1GB for application + logs
|
||||||
|
- **Network**: Stable internet connection
|
||||||
|
|
||||||
|
### Do I need coding experience?
|
||||||
|
No! NOFX has a web UI for all configuration. However, basic command line knowledge helps with setup and troubleshooting.
|
||||||
|
|
||||||
|
### How do I get API keys?
|
||||||
|
1. **Binance**: Account → API Management → Create API → Enable Futures
|
||||||
|
2. **Hyperliquid**: Visit [Hyperliquid App](https://app.hyperliquid.xyz/) → API Settings
|
||||||
|
|
||||||
|
### Should I use a subaccount?
|
||||||
|
**Recommended**: Yes, use a subaccount dedicated to NOFX for better risk isolation. However, note that some subaccounts have restrictions (e.g., 5x max leverage on Binance).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Trading Questions
|
||||||
|
|
||||||
|
### Why isn't my trader making any trades?
|
||||||
|
Common reasons:
|
||||||
|
- AI decided to "wait" due to market conditions
|
||||||
|
- Insufficient balance or margin
|
||||||
|
- Position limits reached (default: max 3 positions)
|
||||||
|
- See detailed diagnostics in [Troubleshooting Guide](TROUBLESHOOTING.md#-ai-always-says-wait--hold)
|
||||||
|
|
||||||
|
### How often does the AI make decisions?
|
||||||
|
Configurable! Default is every **3-5 minutes**. Too frequent = overtrading, too slow = missed opportunities.
|
||||||
|
|
||||||
|
### Can I customize the trading strategy?
|
||||||
|
Yes! You can:
|
||||||
|
- Adjust leverage settings
|
||||||
|
- Modify coin selection pool
|
||||||
|
- Change decision intervals
|
||||||
|
- Customize system prompts (advanced)
|
||||||
|
|
||||||
|
### What's the maximum number of concurrent positions?
|
||||||
|
Default: **3 positions**. This is a soft limit defined in the AI prompt, not hard-coded. See `decision/engine.go:266`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Issues
|
||||||
|
|
||||||
|
### Binance Position Mode Error (code=-4061)
|
||||||
|
|
||||||
|
**Error**: `Order's position side does not match user's setting`
|
||||||
|
|
||||||
|
**Solution**: Switch to **Hedge Mode** (双向持仓)
|
||||||
|
1. Login to [Binance Futures](https://www.binance.com/en/futures/BTCUSDT)
|
||||||
|
2. Click **⚙️ Preferences** (top right)
|
||||||
|
3. Select **Position Mode** → **Hedge Mode**
|
||||||
|
4. ⚠️ Close all positions first
|
||||||
|
|
||||||
|
**Why**: NOFX uses `PositionSide(LONG/SHORT)` which requires Hedge Mode.
|
||||||
|
|
||||||
|
See [Issue #202](https://github.com/tinkle-community/nofx/issues/202) and [Troubleshooting Guide](TROUBLESHOOTING.md#-only-opening-short-positions-issue-202).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Backend won't start / Port already in use
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Check what's using port 8080
|
||||||
|
lsof -i :8080
|
||||||
|
|
||||||
|
# Change port in .env
|
||||||
|
NOFX_BACKEND_PORT=8081
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Frontend shows "Loading..." forever
|
||||||
|
|
||||||
|
**Quick Check**:
|
||||||
|
```bash
|
||||||
|
# Is backend running?
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
|
||||||
|
# Should return: {"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
If not, check [Troubleshooting Guide](TROUBLESHOOTING.md#-frontend-cant-connect-to-backend).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Database locked error
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
# Stop all NOFX processes
|
||||||
|
docker compose down
|
||||||
|
# OR
|
||||||
|
pkill nofx
|
||||||
|
|
||||||
|
# Restart
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI & Model Questions
|
||||||
|
|
||||||
|
### Which AI models are supported?
|
||||||
|
- **DeepSeek** (recommended for cost/performance)
|
||||||
|
- **Qwen** (Alibaba Cloud Tongyi Qianwen)
|
||||||
|
- **Custom OpenAI-compatible APIs** (can be used for OpenAI, Claude via proxy, or other providers)
|
||||||
|
|
||||||
|
### How much do API calls cost?
|
||||||
|
Depends on your model and decision frequency:
|
||||||
|
- **DeepSeek**: ~$0.10-0.50 per day (1 trader, 5min intervals)
|
||||||
|
- **Qwen**: ~$0.20-0.80 per day
|
||||||
|
- **Custom API** (e.g., OpenAI GPT-4): ~$2-5 per day
|
||||||
|
|
||||||
|
*Estimates based on typical usage. Actual costs vary by provider and usage.*
|
||||||
|
|
||||||
|
### Can I use multiple AI models?
|
||||||
|
Yes! Each trader can use a different AI model. You can even A/B test different models.
|
||||||
|
|
||||||
|
### Does the AI learn from its mistakes?
|
||||||
|
Yes, to some extent. NOFX provides historical performance feedback in each decision prompt, allowing the AI to adjust its strategy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data & Privacy
|
||||||
|
|
||||||
|
### Where is my data stored?
|
||||||
|
All data is stored **locally** on your machine in SQLite databases:
|
||||||
|
- `config.db` - Trader configurations
|
||||||
|
- `trading.db` - Trade history
|
||||||
|
- `decision_logs/` - AI decision records
|
||||||
|
|
||||||
|
### Is my API key secure?
|
||||||
|
API keys are stored in local databases. Never share your databases or `.env` files. We recommend using API keys with IP whitelist restrictions.
|
||||||
|
|
||||||
|
### Can I export my trading history?
|
||||||
|
Yes! Trading data is in SQLite format. You can query it directly:
|
||||||
|
```bash
|
||||||
|
sqlite3 trading.db "SELECT * FROM trades;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Where can I find detailed troubleshooting?
|
||||||
|
See the comprehensive [Troubleshooting Guide](TROUBLESHOOTING.md) for:
|
||||||
|
- Step-by-step diagnostics
|
||||||
|
- Log collection methods
|
||||||
|
- Common error solutions
|
||||||
|
- Emergency reset procedures
|
||||||
|
|
||||||
|
### How do I report a bug?
|
||||||
|
1. Check [Troubleshooting Guide](TROUBLESHOOTING.md) first
|
||||||
|
2. Search [existing issues](https://github.com/tinkle-community/nofx/issues)
|
||||||
|
3. If not found, use our [Bug Report Template](../../.github/ISSUE_TEMPLATE/bug_report.md)
|
||||||
|
|
||||||
|
### Where can I get help?
|
||||||
|
- [GitHub Discussions](https://github.com/tinkle-community/nofx/discussions)
|
||||||
|
- [Telegram Community](https://t.me/nofx_dev_community)
|
||||||
|
- [GitHub Issues](https://github.com/tinkle-community/nofx/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
### Can I contribute to NOFX?
|
||||||
|
Yes! We welcome contributions:
|
||||||
|
- Bug fixes and features
|
||||||
|
- Documentation improvements
|
||||||
|
- Translations
|
||||||
|
- See [Contributing Guide](../CONTRIBUTING.md)
|
||||||
|
|
||||||
|
### How do I suggest new features?
|
||||||
|
Open a [Feature Request](https://github.com/tinkle-community/nofx/issues/new/choose) with your idea!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** 2025-11-02
|
||||||
|
|||||||
@@ -1,25 +1,206 @@
|
|||||||
# 常见问题
|
# 常见问题(FAQ)
|
||||||
|
|
||||||
## 币安持仓模式错误 (code=-4061)
|
快速解答常见问题。详细故障排查请参考[故障排查指南](TROUBLESHOOTING.zh-CN.md)。
|
||||||
|
|
||||||
**错误信息**:`Order's position side does not match user's setting`
|
|
||||||
|
|
||||||
**原因**:系统需要使用双向持仓模式,但您的币安账户设置为单向持仓。
|
|
||||||
|
|
||||||
### 解决方法
|
|
||||||
|
|
||||||
1. 登录 [币安合约交易平台](https://www.binance.com/zh-CN/futures/BTCUSDT)
|
|
||||||
|
|
||||||
2. 点击右上角的 **⚙️ 偏好设置**
|
|
||||||
|
|
||||||
3. 选择 **持仓模式**
|
|
||||||
|
|
||||||
4. 切换为 **双向持仓** (Hedge Mode)
|
|
||||||
|
|
||||||
5. 确认切换
|
|
||||||
|
|
||||||
**注意**:切换前必须先平掉所有持仓。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
更多问题请查看 [GitHub Issues](https://github.com/tinkle-community/nofx/issues)
|
## 基础问题
|
||||||
|
|
||||||
|
### NOFX 是什么?
|
||||||
|
NOFX 是一个 AI 驱动的加密货币交易机器人,使用大语言模型(LLM)在期货市场进行交易决策。
|
||||||
|
|
||||||
|
### 支持哪些交易所?
|
||||||
|
- ✅ 币安合约(Binance Futures)
|
||||||
|
- ✅ Hyperliquid
|
||||||
|
- 🚧 更多交易所开发中
|
||||||
|
|
||||||
|
### NOFX 能盈利吗?
|
||||||
|
AI 交易是**实验性**的,**不保证盈利**。请始终用小额资金测试,不要投入超过您承受能力的资金。
|
||||||
|
|
||||||
|
### 可以同时运行多个交易员吗?
|
||||||
|
可以!NOFX 支持运行多个交易员,每个可配置不同的 AI 模型和交易策略。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安装与配置
|
||||||
|
|
||||||
|
### 系统要求是什么?
|
||||||
|
- **操作系统**:Linux、macOS 或 Windows(推荐 Docker)
|
||||||
|
- **内存**:最低 2GB,推荐 4GB
|
||||||
|
- **硬盘**:应用 + 日志需要 1GB
|
||||||
|
- **网络**:稳定的互联网连接
|
||||||
|
|
||||||
|
### 需要编程经验吗?
|
||||||
|
不需要!NOFX 有 Web 界面进行所有配置。但基础的命令行知识有助于安装和故障排查。
|
||||||
|
|
||||||
|
### 如何获取 API 密钥?
|
||||||
|
1. **币安**:账户 → API 管理 → 创建 API → 启用合约
|
||||||
|
2. **Hyperliquid**:访问 [Hyperliquid App](https://app.hyperliquid.xyz/) → API 设置
|
||||||
|
|
||||||
|
### 应该使用子账户吗?
|
||||||
|
**推荐**:是的,使用专门的子账户运行 NOFX 可以更好地隔离风险。但请注意,某些子账户有限制(例如币安子账户最高 5 倍杠杆)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 交易问题
|
||||||
|
|
||||||
|
### 为什么我的交易员不开仓?
|
||||||
|
常见原因:
|
||||||
|
- AI 根据市场情况决定"等待"
|
||||||
|
- 余额或保证金不足
|
||||||
|
- 达到持仓上限(默认最多 3 个仓位)
|
||||||
|
- 详细诊断请查看[故障排查指南](TROUBLESHOOTING.zh-CN.md#-ai-总是说等待持有)
|
||||||
|
|
||||||
|
### AI 多久做一次决策?
|
||||||
|
可配置!默认是每 **3-5 分钟**。太频繁 = 过度交易,太慢 = 错过机会。
|
||||||
|
|
||||||
|
### 可以自定义交易策略吗?
|
||||||
|
可以!您可以:
|
||||||
|
- 调整杠杆设置
|
||||||
|
- 修改币种选择池
|
||||||
|
- 更改决策间隔
|
||||||
|
- 自定义系统提示词(高级)
|
||||||
|
|
||||||
|
### 最多可以同时持有多少个仓位?
|
||||||
|
默认:**3 个仓位**。这是 AI 提示词中的软限制,不是硬编码。参见 `decision/engine.go:266`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术问题
|
||||||
|
|
||||||
|
### 币安持仓模式错误 (code=-4061)
|
||||||
|
|
||||||
|
**错误信息**:`Order's position side does not match user's setting`
|
||||||
|
|
||||||
|
**解决方法**:切换为**双向持仓**模式
|
||||||
|
1. 登录[币安合约](https://www.binance.com/zh-CN/futures/BTCUSDT)
|
||||||
|
2. 点击右上角 **⚙️ 偏好设置**
|
||||||
|
3. 选择 **持仓模式** → **双向持仓**
|
||||||
|
4. ⚠️ 先平掉所有持仓
|
||||||
|
|
||||||
|
**原因**:NOFX 使用 `PositionSide(LONG/SHORT)`,需要双向持仓模式。
|
||||||
|
|
||||||
|
参见 [Issue #202](https://github.com/tinkle-community/nofx/issues/202) 和[故障排查指南](TROUBLESHOOTING.zh-CN.md#-只开空单-issue-202)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 后端无法启动 / 端口被占用
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
```bash
|
||||||
|
# 查看占用端口的进程
|
||||||
|
lsof -i :8080
|
||||||
|
|
||||||
|
# 修改 .env 中的端口
|
||||||
|
NOFX_BACKEND_PORT=8081
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 前端一直显示"加载中..."
|
||||||
|
|
||||||
|
**快速检查**:
|
||||||
|
```bash
|
||||||
|
# 后端是否运行?
|
||||||
|
curl http://localhost:8080/api/health
|
||||||
|
|
||||||
|
# 应该返回:{"status":"ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果不是,查看[故障排查指南](TROUBLESHOOTING.zh-CN.md#-前端无法连接后端)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 数据库锁定错误
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
```bash
|
||||||
|
# 停止所有 NOFX 进程
|
||||||
|
docker compose down
|
||||||
|
# 或
|
||||||
|
pkill nofx
|
||||||
|
|
||||||
|
# 重启
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI 与模型问题
|
||||||
|
|
||||||
|
### 支持哪些 AI 模型?
|
||||||
|
- **DeepSeek**(推荐性价比)
|
||||||
|
- **Qwen**(阿里云通义千问)
|
||||||
|
- **自定义 OpenAI 兼容 API**(可用于 OpenAI、通过代理的 Claude 或其他提供商)
|
||||||
|
|
||||||
|
### API 调用成本是多少?
|
||||||
|
取决于您的模型和决策频率:
|
||||||
|
- **DeepSeek**:每天约 $0.10-0.50(1 个交易员,5 分钟间隔)
|
||||||
|
- **Qwen**:每天约 $0.20-0.80
|
||||||
|
- **自定义 API**(例如 OpenAI GPT-4):每天约 $2-5
|
||||||
|
|
||||||
|
*基于典型使用的估算。实际成本因提供商和使用量而异。*
|
||||||
|
|
||||||
|
### 可以使用多个 AI 模型吗?
|
||||||
|
可以!每个交易员可以使用不同的 AI 模型。您甚至可以 A/B 测试不同模型。
|
||||||
|
|
||||||
|
### AI 会从错误中学习吗?
|
||||||
|
会的,在一定程度上。NOFX 在每次决策提示中提供历史表现反馈,允许 AI 调整策略。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据与隐私
|
||||||
|
|
||||||
|
### 我的数据存储在哪里?
|
||||||
|
所有数据都**本地存储**在您的机器上,使用 SQLite 数据库:
|
||||||
|
- `config.db` - 交易员配置
|
||||||
|
- `trading.db` - 交易历史
|
||||||
|
- `decision_logs/` - AI 决策记录
|
||||||
|
|
||||||
|
### API 密钥安全吗?
|
||||||
|
API 密钥存储在本地数据库中。永远不要分享您的数据库或 `.env` 文件。我们建议使用带 IP 白名单限制的 API 密钥。
|
||||||
|
|
||||||
|
### 可以导出交易历史吗?
|
||||||
|
可以!交易数据是 SQLite 格式。您可以直接查询:
|
||||||
|
```bash
|
||||||
|
sqlite3 trading.db "SELECT * FROM trades;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 故障排查
|
||||||
|
|
||||||
|
### 在哪里可以找到详细的故障排查?
|
||||||
|
查看全面的[故障排查指南](TROUBLESHOOTING.zh-CN.md),包含:
|
||||||
|
- 分步诊断方法
|
||||||
|
- 日志收集方法
|
||||||
|
- 常见错误解决方案
|
||||||
|
- 紧急重置步骤
|
||||||
|
|
||||||
|
### 如何报告 Bug?
|
||||||
|
1. 先查看[故障排查指南](TROUBLESHOOTING.zh-CN.md)
|
||||||
|
2. 搜索[现有 Issues](https://github.com/tinkle-community/nofx/issues)
|
||||||
|
3. 如果没找到,使用我们的 [Bug 报告模板](../../.github/ISSUE_TEMPLATE/bug_report.md)
|
||||||
|
|
||||||
|
### 在哪里可以获得帮助?
|
||||||
|
- [GitHub Discussions](https://github.com/tinkle-community/nofx/discussions)
|
||||||
|
- [Telegram 社区](https://t.me/nofx_dev_community)
|
||||||
|
- [GitHub Issues](https://github.com/tinkle-community/nofx/issues)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
### 可以为 NOFX 贡献代码吗?
|
||||||
|
可以!我们欢迎贡献:
|
||||||
|
- Bug 修复和新功能
|
||||||
|
- 文档改进
|
||||||
|
- 翻译
|
||||||
|
- 查看[贡献指南](../CONTRIBUTING.md)
|
||||||
|
|
||||||
|
### 如何建议新功能?
|
||||||
|
提交 [Feature Request](https://github.com/tinkle-community/nofx/issues/new/choose) 说明您的想法!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**最后更新:** 2025-11-02
|
||||||
|
|||||||
@@ -117,11 +117,12 @@ NOFX теперь поддерживает **три основные биржи*
|
|||||||
- 🌐 **Поддержка нескольких цепей** - торгуйте на вашей любимой EVM цепи
|
- 🌐 **Поддержка нескольких цепей** - торгуйте на вашей любимой EVM цепи
|
||||||
|
|
||||||
**Быстрый старт:**
|
**Быстрый старт:**
|
||||||
1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
1. Зарегистрируйтесь по [реферальной ссылке Aster](https://www.asterdex.com/en/referral/fdfc0e) (получите скидку на комиссии!)
|
||||||
2. Подключите основной кошелек и создайте API кошелек
|
2. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||||
3. Скопируйте адрес API Signer и приватный ключ
|
3. Подключите основной кошелек и создайте API кошелек
|
||||||
4. Установите `"exchange": "aster"` в config.json
|
4. Скопируйте адрес API Signer и приватный ключ
|
||||||
5. Добавьте `"aster_user"`, `"aster_signer"` и `"aster_private_key"`
|
5. Установите `"exchange": "aster"` в config.json
|
||||||
|
6. Добавьте `"aster_user"`, `"aster_signer"` и `"aster_private_key"`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -399,7 +400,7 @@ cd ..
|
|||||||
|
|
||||||
**Как получить Qwen API ключ:**
|
**Как получить Qwen API ключ:**
|
||||||
|
|
||||||
1. **Посетите**: [https://dashscope.aliyuncs.com](https://dashscope.aliyuncs.com)
|
1. **Посетите**: [https://dashscope.console.aliyun.com](https://dashscope.console.aliyun.com)
|
||||||
2. **Зарегистрируйтесь**: Используя аккаунт Alibaba Cloud
|
2. **Зарегистрируйтесь**: Используя аккаунт Alibaba Cloud
|
||||||
3. **Активируйте сервис**: Активируйте DashScope сервис
|
3. **Активируйте сервис**: Активируйте DashScope сервис
|
||||||
4. **Создайте API ключ**:
|
4. **Создайте API ключ**:
|
||||||
@@ -534,12 +535,13 @@ cp config.example.jsonc config.json
|
|||||||
- 🌐 Поддержка нескольких цепей (ETH, BSC, Polygon)
|
- 🌐 Поддержка нескольких цепей (ETH, BSC, Polygon)
|
||||||
- 🌍 Не нужна KYC
|
- 🌍 Не нужна KYC
|
||||||
|
|
||||||
**Шаг 1**: Создайте Aster API кошелек
|
**Шаг 1**: Зарегистрируйтесь и создайте Aster API кошелек
|
||||||
|
|
||||||
1. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
1. Зарегистрируйтесь по [реферальной ссылке Aster](https://www.asterdex.com/en/referral/fdfc0e) (получите скидку на комиссии!)
|
||||||
2. Подключите основной кошелек (MetaMask, WalletConnect и т.д.)
|
2. Посетите [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||||
3. Нажмите "Создать API кошелек"
|
3. Подключите основной кошелек (MetaMask, WalletConnect и т.д.)
|
||||||
4. **Сохраните эти 3 элемента немедленно:**
|
4. Нажмите "Создать API кошелек"
|
||||||
|
5. **Сохраните эти 3 элемента немедленно:**
|
||||||
- Адрес основного кошелька (User)
|
- Адрес основного кошелька (User)
|
||||||
- Адрес API кошелька (Signer)
|
- Адрес API кошелька (Signer)
|
||||||
- Приватный ключ API кошелька (⚠️ показывается только один раз!)
|
- Приватный ключ API кошелька (⚠️ показывается только один раз!)
|
||||||
@@ -556,9 +558,9 @@ cp config.example.jsonc config.json
|
|||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
"aster_user": "0xYOUR_MAIN_WALLET_ADDRESS_HERE",
|
||||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
"aster_signer": "0xYOUR_API_WALLET_SIGNER_ADDRESS_HERE",
|
||||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
"aster_private_key": "your_api_wallet_private_key_without_0x_prefix",
|
||||||
|
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -1094,7 +1096,7 @@ sudo apt-get install libta-lib0-dev
|
|||||||
|
|
||||||
- [Binance API](https://binance-docs.github.io/apidocs/futures/en/) - Binance Futures API
|
- [Binance API](https://binance-docs.github.io/apidocs/futures/en/) - Binance Futures API
|
||||||
- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API
|
- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API
|
||||||
- [Qwen](https://dashscope.aliyuncs.com/) - Alibaba Cloud Qwen
|
- [Qwen](https://dashscope.console.aliyun.com/) - Alibaba Cloud Qwen
|
||||||
- [TA-Lib](https://ta-lib.org/) - Библиотека технических индикаторов
|
- [TA-Lib](https://ta-lib.org/) - Библиотека технических индикаторов
|
||||||
- [Recharts](https://recharts.org/) - Библиотека графиков React
|
- [Recharts](https://recharts.org/) - Библиотека графиков React
|
||||||
|
|
||||||
|
|||||||
@@ -118,11 +118,12 @@ NOFX тепер підтримує **три основні біржі**: Binance
|
|||||||
- 🌐 **Підтримка кількох ланцюгів** - торгуйте на вашому улюбленому EVM ланцюзі
|
- 🌐 **Підтримка кількох ланцюгів** - торгуйте на вашому улюбленому EVM ланцюзі
|
||||||
|
|
||||||
**Швидкий старт:**
|
**Швидкий старт:**
|
||||||
1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
1. Зареєструйтеся за [реферальним посиланням Aster](https://www.asterdex.com/en/referral/fdfc0e) (отримайте знижку на комісії!)
|
||||||
2. Підключіть основний гаманець і створіть API гаманець
|
2. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||||
3. Скопіюйте адресу API Signer та приватний ключ
|
3. Підключіть основний гаманець і створіть API гаманець
|
||||||
4. Встановіть `"exchange": "aster"` в config.json
|
4. Скопіюйте адресу API Signer та приватний ключ
|
||||||
5. Додайте `"aster_user"`, `"aster_signer"` та `"aster_private_key"`
|
5. Встановіть `"exchange": "aster"` в config.json
|
||||||
|
6. Додайте `"aster_user"`, `"aster_signer"` та `"aster_private_key"`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -402,7 +403,7 @@ cd ..
|
|||||||
|
|
||||||
**Як отримати Qwen API ключ:**
|
**Як отримати Qwen API ключ:**
|
||||||
|
|
||||||
1. **Відвідайте**: [https://dashscope.aliyuncs.com](https://dashscope.aliyuncs.com)
|
1. **Відвідайте**: [https://dashscope.console.aliyun.com](https://dashscope.console.aliyun.com)
|
||||||
2. **Зареєструйтеся**: Використовуючи акаунт Alibaba Cloud
|
2. **Зареєструйтеся**: Використовуючи акаунт Alibaba Cloud
|
||||||
3. **Активуйте сервіс**: Активуйте DashScope сервіс
|
3. **Активуйте сервіс**: Активуйте DashScope сервіс
|
||||||
4. **Створіть API ключ**:
|
4. **Створіть API ключ**:
|
||||||
@@ -537,12 +538,13 @@ cp config.example.jsonc config.json
|
|||||||
- 🌐 Підтримка кількох ланцюгів (ETH, BSC, Polygon)
|
- 🌐 Підтримка кількох ланцюгів (ETH, BSC, Polygon)
|
||||||
- 🌍 Не потрібна KYC
|
- 🌍 Не потрібна KYC
|
||||||
|
|
||||||
**Крок 1**: Створіть Aster API гаманець
|
**Крок 1**: Зареєструйтеся та створіть Aster API гаманець
|
||||||
|
|
||||||
1. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
1. Зареєструйтеся за [реферальним посиланням Aster](https://www.asterdex.com/en/referral/fdfc0e) (отримайте знижку на комісії!)
|
||||||
2. Підключіть основний гаманець (MetaMask, WalletConnect тощо)
|
2. Відвідайте [Aster API Wallet](https://www.asterdex.com/en/api-wallet)
|
||||||
3. Натисніть "Створити API гаманець"
|
3. Підключіть основний гаманець (MetaMask, WalletConnect тощо)
|
||||||
4. **Збережіть ці 3 елементи негайно:**
|
4. Натисніть "Створити API гаманець"
|
||||||
|
5. **Збережіть ці 3 елементи негайно:**
|
||||||
- Адреса основного гаманця (User)
|
- Адреса основного гаманця (User)
|
||||||
- Адреса API гаманця (Signer)
|
- Адреса API гаманця (Signer)
|
||||||
- Приватний ключ API гаманця (⚠️ показується лише один раз!)
|
- Приватний ключ API гаманця (⚠️ показується лише один раз!)
|
||||||
@@ -559,9 +561,9 @@ cp config.example.jsonc config.json
|
|||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
"aster_user": "0xYOUR_MAIN_WALLET_ADDRESS_HERE",
|
||||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
"aster_signer": "0xYOUR_API_WALLET_SIGNER_ADDRESS_HERE",
|
||||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
"aster_private_key": "your_api_wallet_private_key_without_0x_prefix",
|
||||||
|
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
|
|||||||
@@ -126,11 +126,12 @@ NOFX现已支持**三大交易所**:Binance、Hyperliquid和Aster DEX!
|
|||||||
- 🌐 **多链支持** - 在你喜欢的EVM链上交易
|
- 🌐 **多链支持** - 在你喜欢的EVM链上交易
|
||||||
|
|
||||||
**快速开始:**
|
**快速开始:**
|
||||||
1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
|
1. 通过[推荐链接注册Aster](https://www.asterdex.com/en/referral/fdfc0e)(享手续费优惠)
|
||||||
2. 连接你的主钱包并创建API钱包
|
2. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
|
||||||
3. 复制API Signer地址和私钥
|
3. 连接你的主钱包并创建API钱包
|
||||||
4. ~~在config.json中设置`"exchange": "aster"`~~ *通过Web界面配置*
|
4. 复制API Signer地址和私钥
|
||||||
5. 添加`"aster_user"`、`"aster_signer"`和`"aster_private_key"`
|
5. 在config.json中设置`"exchange": "aster"`
|
||||||
|
6. 添加`"aster_user"`、`"aster_signer"`和`"aster_private_key"`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -398,7 +399,7 @@ cd ..
|
|||||||
|
|
||||||
**如何获取Qwen API密钥:**
|
**如何获取Qwen API密钥:**
|
||||||
|
|
||||||
1. **访问**:[https://dashscope.aliyuncs.com](https://dashscope.aliyuncs.com)
|
1. **访问**:[https://dashscope.console.aliyun.com](https://dashscope.console.aliyun.com)
|
||||||
2. **注册**:使用阿里云账户注册
|
2. **注册**:使用阿里云账户注册
|
||||||
3. **开通服务**:激活DashScope服务
|
3. **开通服务**:激活DashScope服务
|
||||||
4. **创建API密钥**:
|
4. **创建API密钥**:
|
||||||
@@ -535,12 +536,13 @@ cp config.example.jsonc config.json
|
|||||||
- 🌐 多链支持(ETH、BSC、Polygon)
|
- 🌐 多链支持(ETH、BSC、Polygon)
|
||||||
- 🌍 无需KYC
|
- 🌍 无需KYC
|
||||||
|
|
||||||
**步骤1**:创建Aster API钱包
|
**步骤1**:注册并创建Aster API钱包
|
||||||
|
|
||||||
1. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
|
1. 通过[推荐链接注册Aster](https://www.asterdex.com/en/referral/fdfc0e)(享手续费优惠)
|
||||||
2. 连接你的主钱包(MetaMask、WalletConnect等)
|
2. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
|
||||||
3. 点击"创建API钱包"
|
3. 连接你的主钱包(MetaMask、WalletConnect等)
|
||||||
4. **立即保存这3项:**
|
4. 点击"创建API钱包"
|
||||||
|
5. **立即保存这3项:**
|
||||||
- 主钱包地址(User)
|
- 主钱包地址(User)
|
||||||
- API钱包地址(Signer)
|
- API钱包地址(Signer)
|
||||||
- API钱包私钥(⚠️ 仅显示一次!)
|
- API钱包私钥(⚠️ 仅显示一次!)
|
||||||
@@ -557,9 +559,9 @@ cp config.example.jsonc config.json
|
|||||||
"ai_model": "deepseek",
|
"ai_model": "deepseek",
|
||||||
"exchange": "aster",
|
"exchange": "aster",
|
||||||
|
|
||||||
"aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e",
|
"aster_user": "0xYOUR_MAIN_WALLET_ADDRESS_HERE",
|
||||||
"aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0",
|
"aster_signer": "0xYOUR_API_WALLET_SIGNER_ADDRESS_HERE",
|
||||||
"aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1",
|
"aster_private_key": "your_api_wallet_private_key_without_0x_prefix",
|
||||||
|
|
||||||
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
"deepseek_key": "sk-xxxxxxxxxxxxx",
|
||||||
"initial_balance": 1000.0,
|
"initial_balance": 1000.0,
|
||||||
@@ -1290,7 +1292,7 @@ MIT License - 详见 [LICENSE](LICENSE) 文件
|
|||||||
|
|
||||||
- [Binance API](https://binance-docs.github.io/apidocs/futures/cn/) - 币安合约API
|
- [Binance API](https://binance-docs.github.io/apidocs/futures/cn/) - 币安合约API
|
||||||
- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API
|
- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API
|
||||||
- [Qwen](https://dashscope.aliyuncs.com/) - 阿里云通义千问
|
- [Qwen](https://dashscope.console.aliyun.com/) - 阿里云通义千问
|
||||||
- [TA-Lib](https://ta-lib.org/) - 技术指标库
|
- [TA-Lib](https://ta-lib.org/) - 技术指标库
|
||||||
- [Recharts](https://recharts.org/) - React图表库
|
- [Recharts](https://recharts.org/) - React图表库
|
||||||
|
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -8,7 +8,8 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
github.com/gorilla/websocket v1.5.3
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/sonirico/go-hyperliquid v0.17.0
|
github.com/sonirico/go-hyperliquid v0.17.0
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.42.0
|
||||||
@@ -26,6 +27,7 @@ require (
|
|||||||
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
|
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
|
||||||
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
github.com/elastic/go-sysinfo v1.15.4 // indirect
|
||||||
github.com/elastic/go-windows v1.0.2 // indirect
|
github.com/elastic/go-windows v1.0.2 // indirect
|
||||||
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
|
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
|
||||||
@@ -37,7 +39,6 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.4 // indirect
|
github.com/goccy/go-json v0.10.4 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
|
||||||
github.com/holiman/uint256 v1.3.2 // indirect
|
github.com/holiman/uint256 v1.3.2 // indirect
|
||||||
github.com/joho/godotenv v1.5.1 // indirect
|
github.com/joho/godotenv v1.5.1 // indirect
|
||||||
github.com/josharian/intern v1.0.0 // indirect
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
@@ -55,6 +56,7 @@ require (
|
|||||||
github.com/prometheus/procfs v0.17.0 // indirect
|
github.com/prometheus/procfs v0.17.0 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rs/zerolog v1.34.0 // indirect
|
github.com/rs/zerolog v1.34.0 // indirect
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sonirico/vago v0.9.0 // indirect
|
github.com/sonirico/vago v0.9.0 // indirect
|
||||||
@@ -78,4 +80,8 @@ require (
|
|||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
howett.net/plist v1.0.1 // indirect
|
howett.net/plist v1.0.1 // indirect
|
||||||
|
modernc.org/libc v1.37.6 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/sqlite v1.28.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
8
go.sum
8
go.sum
@@ -32,6 +32,7 @@ github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U
|
|||||||
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=
|
github.com/elastic/go-sysinfo v1.15.4 h1:A3zQcunCxik14MgXu39cXFXcIw2sFXZ0zL886eyiv1Q=
|
||||||
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
|
github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU=
|
||||||
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
|
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
|
||||||
@@ -118,6 +119,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
@@ -144,6 +147,7 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
|||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -228,3 +232,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
|
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
|
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
|
||||||
|
|||||||
33
main.go
33
main.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"nofx/auth"
|
"nofx/auth"
|
||||||
"nofx/config"
|
"nofx/config"
|
||||||
"nofx/manager"
|
"nofx/manager"
|
||||||
|
"nofx/market"
|
||||||
"nofx/pool"
|
"nofx/pool"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -30,11 +31,13 @@ type ConfigFile struct {
|
|||||||
DefaultCoins []string `json:"default_coins"`
|
DefaultCoins []string `json:"default_coins"`
|
||||||
CoinPoolAPIURL string `json:"coin_pool_api_url"`
|
CoinPoolAPIURL string `json:"coin_pool_api_url"`
|
||||||
OITopAPIURL string `json:"oi_top_api_url"`
|
OITopAPIURL string `json:"oi_top_api_url"`
|
||||||
|
InsideCoins bool `json:"inside_coins"`
|
||||||
MaxDailyLoss float64 `json:"max_daily_loss"`
|
MaxDailyLoss float64 `json:"max_daily_loss"`
|
||||||
MaxDrawdown float64 `json:"max_drawdown"`
|
MaxDrawdown float64 `json:"max_drawdown"`
|
||||||
StopTradingMinutes int `json:"stop_trading_minutes"`
|
StopTradingMinutes int `json:"stop_trading_minutes"`
|
||||||
Leverage LeverageConfig `json:"leverage"`
|
Leverage LeverageConfig `json:"leverage"`
|
||||||
JWTSecret string `json:"jwt_secret"`
|
JWTSecret string `json:"jwt_secret"`
|
||||||
|
DataKLineTime string `json:"data_k_line_time"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// syncConfigToDatabase 从config.json读取配置并同步到数据库
|
// syncConfigToDatabase 从config.json读取配置并同步到数据库
|
||||||
@@ -66,6 +69,7 @@ func syncConfigToDatabase(database *config.Database) error {
|
|||||||
"use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins),
|
"use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins),
|
||||||
"coin_pool_api_url": configFile.CoinPoolAPIURL,
|
"coin_pool_api_url": configFile.CoinPoolAPIURL,
|
||||||
"oi_top_api_url": configFile.OITopAPIURL,
|
"oi_top_api_url": configFile.OITopAPIURL,
|
||||||
|
"inside_coins": fmt.Sprintf("%t", configFile.InsideCoins),
|
||||||
"max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss),
|
"max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss),
|
||||||
"max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown),
|
"max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown),
|
||||||
"stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes),
|
"stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes),
|
||||||
@@ -179,7 +183,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pool.SetDefaultCoins(defaultCoins)
|
pool.SetDefaultCoins(defaultCoins)
|
||||||
|
|
||||||
// 设置是否使用默认主流币种
|
// 设置是否使用默认主流币种
|
||||||
pool.SetUseDefaultCoins(useDefaultCoins)
|
pool.SetUseDefaultCoins(useDefaultCoins)
|
||||||
if useDefaultCoins {
|
if useDefaultCoins {
|
||||||
@@ -208,37 +211,26 @@ func main() {
|
|||||||
log.Fatalf("❌ 加载交易员失败: %v", err)
|
log.Fatalf("❌ 加载交易员失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有用户的交易员配置(用于显示)
|
// 获取数据库中的所有交易员配置(用于显示,使用default用户)
|
||||||
userIDs, err := database.GetAllUsers()
|
traders, err := database.GetTraders("default")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("⚠️ 获取用户列表失败: %v", err)
|
log.Fatalf("❌ 获取交易员列表失败: %v", err)
|
||||||
userIDs = []string{"default"} // 回退到default用户
|
|
||||||
}
|
|
||||||
|
|
||||||
var allTraders []*config.TraderRecord
|
|
||||||
for _, userID := range userIDs {
|
|
||||||
traders, err := database.GetTraders(userID)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("⚠️ 获取用户 %s 的交易员失败: %v", userID, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
allTraders = append(allTraders, traders...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示加载的交易员信息
|
// 显示加载的交易员信息
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("🤖 数据库中的AI交易员配置:")
|
fmt.Println("🤖 数据库中的AI交易员配置:")
|
||||||
if len(allTraders) == 0 {
|
if len(traders) == 0 {
|
||||||
fmt.Println(" • 暂无配置的交易员,请通过Web界面创建")
|
fmt.Println(" • 暂无配置的交易员,请通过Web界面创建")
|
||||||
} else {
|
} else {
|
||||||
for _, trader := range allTraders {
|
for _, trader := range traders {
|
||||||
status := "停止"
|
status := "停止"
|
||||||
if trader.IsRunning {
|
if trader.IsRunning {
|
||||||
status = "运行中"
|
status = "运行中"
|
||||||
}
|
}
|
||||||
fmt.Printf(" • %s (%s + %s) - 用户: %s - 初始资金: %.0f USDT [%s]\n",
|
fmt.Printf(" • %s (%s + %s) - 初始资金: %.0f USDT [%s]\n",
|
||||||
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
|
trader.Name, strings.ToUpper(trader.AIModelID), strings.ToUpper(trader.ExchangeID),
|
||||||
trader.UserID, trader.InitialBalance, status)
|
trader.InitialBalance, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +263,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// 启动流行情数据 - 默认使用所有交易员设置的币种 如果没有设置币种 则优先使用系统默认
|
||||||
|
go market.NewWSMonitor(150).Start(database.GetCustomCoins())
|
||||||
|
//go market.NewWSMonitor(150).Start([]string{}) //这里是一个使用方式 传入空的话 则使用market市场的所有币种
|
||||||
// 设置优雅退出
|
// 设置优雅退出
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|||||||
150
market/api_client.go
Normal file
150
market/api_client.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseURL = "https://fapi.binance.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIClient struct {
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIClient() *APIClient {
|
||||||
|
return &APIClient{
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) GetExchangeInfo() (*ExchangeInfo, error) {
|
||||||
|
url := fmt.Sprintf("%s/fapi/v1/exchangeInfo", baseURL)
|
||||||
|
resp, err := c.client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var exchangeInfo ExchangeInfo
|
||||||
|
err = json.Unmarshal(body, &exchangeInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &exchangeInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) GetKlines(symbol, interval string, limit int) ([]Kline, error) {
|
||||||
|
url := fmt.Sprintf("%s/fapi/v1/klines", baseURL)
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("symbol", symbol)
|
||||||
|
q.Add("interval", interval)
|
||||||
|
q.Add("limit", strconv.Itoa(limit))
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var klineResponses []KlineResponse
|
||||||
|
err = json.Unmarshal(body, &klineResponses)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var klines []Kline
|
||||||
|
for _, kr := range klineResponses {
|
||||||
|
kline, err := parseKline(kr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("解析K线数据失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
klines = append(klines, kline)
|
||||||
|
}
|
||||||
|
|
||||||
|
return klines, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKline(kr KlineResponse) (Kline, error) {
|
||||||
|
var kline Kline
|
||||||
|
|
||||||
|
if len(kr) < 11 {
|
||||||
|
return kline, fmt.Errorf("invalid kline data")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析各个字段
|
||||||
|
kline.OpenTime = int64(kr[0].(float64))
|
||||||
|
kline.Open, _ = strconv.ParseFloat(kr[1].(string), 64)
|
||||||
|
kline.High, _ = strconv.ParseFloat(kr[2].(string), 64)
|
||||||
|
kline.Low, _ = strconv.ParseFloat(kr[3].(string), 64)
|
||||||
|
kline.Close, _ = strconv.ParseFloat(kr[4].(string), 64)
|
||||||
|
kline.Volume, _ = strconv.ParseFloat(kr[5].(string), 64)
|
||||||
|
kline.CloseTime = int64(kr[6].(float64))
|
||||||
|
kline.QuoteVolume, _ = strconv.ParseFloat(kr[7].(string), 64)
|
||||||
|
kline.Trades = int(kr[8].(float64))
|
||||||
|
kline.TakerBuyBaseVolume, _ = strconv.ParseFloat(kr[9].(string), 64)
|
||||||
|
kline.TakerBuyQuoteVolume, _ = strconv.ParseFloat(kr[10].(string), 64)
|
||||||
|
|
||||||
|
return kline, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *APIClient) GetCurrentPrice(symbol string) (float64, error) {
|
||||||
|
url := fmt.Sprintf("%s/fapi/v1/ticker/price", baseURL)
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Add("symbol", symbol)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticker PriceTicker
|
||||||
|
err = json.Unmarshal(body, &ticker)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
price, err := strconv.ParseFloat(ticker.Price, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return price, nil
|
||||||
|
}
|
||||||
202
market/combined_streams.go
Normal file
202
market/combined_streams.go
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CombinedStreamsClient struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
mu sync.RWMutex
|
||||||
|
subscribers map[string]chan []byte
|
||||||
|
reconnect bool
|
||||||
|
done chan struct{}
|
||||||
|
batchSize int // 每批订阅的流数量
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCombinedStreamsClient(batchSize int) *CombinedStreamsClient {
|
||||||
|
return &CombinedStreamsClient{
|
||||||
|
subscribers: make(map[string]chan []byte),
|
||||||
|
reconnect: true,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
batchSize: batchSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CombinedStreamsClient) Connect() error {
|
||||||
|
dialer := websocket.Dialer{
|
||||||
|
HandshakeTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组合流使用不同的端点
|
||||||
|
conn, _, err := dialer.Dial("wss://fstream.binance.com/stream", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("组合流WebSocket连接失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
c.conn = conn
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
log.Println("组合流WebSocket连接成功")
|
||||||
|
go c.readMessages()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchSubscribeKlines 批量订阅K线
|
||||||
|
func (c *CombinedStreamsClient) BatchSubscribeKlines(symbols []string, interval string) error {
|
||||||
|
// 将symbols分批处理
|
||||||
|
batches := c.splitIntoBatches(symbols, c.batchSize)
|
||||||
|
|
||||||
|
for i, batch := range batches {
|
||||||
|
log.Printf("订阅第 %d 批, 数量: %d", i+1, len(batch))
|
||||||
|
|
||||||
|
streams := make([]string, len(batch))
|
||||||
|
for j, symbol := range batch {
|
||||||
|
streams[j] = fmt.Sprintf("%s@kline_%s", strings.ToLower(symbol), interval)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.subscribeStreams(streams); err != nil {
|
||||||
|
return fmt.Errorf("第 %d 批订阅失败: %v", i+1, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批次间延迟,避免被限制
|
||||||
|
if i < len(batches)-1 {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitIntoBatches 将切片分成指定大小的批次
|
||||||
|
func (c *CombinedStreamsClient) splitIntoBatches(symbols []string, batchSize int) [][]string {
|
||||||
|
var batches [][]string
|
||||||
|
|
||||||
|
for i := 0; i < len(symbols); i += batchSize {
|
||||||
|
end := i + batchSize
|
||||||
|
if end > len(symbols) {
|
||||||
|
end = len(symbols)
|
||||||
|
}
|
||||||
|
batches = append(batches, symbols[i:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribeStreams 订阅多个流
|
||||||
|
func (c *CombinedStreamsClient) subscribeStreams(streams []string) error {
|
||||||
|
subscribeMsg := map[string]interface{}{
|
||||||
|
"method": "SUBSCRIBE",
|
||||||
|
"params": streams,
|
||||||
|
"id": time.Now().UnixNano(),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
if c.conn == nil {
|
||||||
|
return fmt.Errorf("WebSocket未连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("订阅流: %v", streams)
|
||||||
|
return c.conn.WriteJSON(subscribeMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CombinedStreamsClient) readMessages() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c.mu.RLock()
|
||||||
|
conn := c.conn
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取组合流消息失败: %v", err)
|
||||||
|
c.handleReconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handleCombinedMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CombinedStreamsClient) handleCombinedMessage(message []byte) {
|
||||||
|
var combinedMsg struct {
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(message, &combinedMsg); err != nil {
|
||||||
|
log.Printf("解析组合消息失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
ch, exists := c.subscribers[combinedMsg.Stream]
|
||||||
|
c.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
select {
|
||||||
|
case ch <- combinedMsg.Data:
|
||||||
|
default:
|
||||||
|
log.Printf("订阅者通道已满: %s", combinedMsg.Stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CombinedStreamsClient) AddSubscriber(stream string, bufferSize int) <-chan []byte {
|
||||||
|
ch := make(chan []byte, bufferSize)
|
||||||
|
c.mu.Lock()
|
||||||
|
c.subscribers[stream] = ch
|
||||||
|
c.mu.Unlock()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CombinedStreamsClient) handleReconnect() {
|
||||||
|
if !c.reconnect {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("组合流尝试重新连接...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
if err := c.Connect(); err != nil {
|
||||||
|
log.Printf("组合流重新连接失败: %v", err)
|
||||||
|
go c.handleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CombinedStreamsClient) Close() {
|
||||||
|
c.reconnect = false
|
||||||
|
close(c.done)
|
||||||
|
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for stream, ch := range c.subscribers {
|
||||||
|
close(ch)
|
||||||
|
delete(c.subscribers, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
308
market/data.go
308
market/data.go
@@ -10,108 +10,20 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Data 市场数据结构
|
|
||||||
type Data struct {
|
|
||||||
Symbol string
|
|
||||||
CurrentPrice float64
|
|
||||||
PriceChange1h float64 // 1小时价格变化百分比
|
|
||||||
PriceChange4h float64 // 4小时价格变化百分比
|
|
||||||
CurrentEMA20 float64
|
|
||||||
CurrentMACD float64
|
|
||||||
CurrentRSI7 float64
|
|
||||||
OpenInterest *OIData
|
|
||||||
FundingRate float64
|
|
||||||
IntradaySeries *IntradayData // 3分钟数据 - 实时价格
|
|
||||||
MidTermSeries15m *MidTermData15m // 15分钟数据 - 短期趋势
|
|
||||||
MidTermSeries1h *MidTermData1h // 1小时数据 - 中期趋势
|
|
||||||
LongerTermContext *LongerTermData // 4小时数据 - 长期趋势
|
|
||||||
}
|
|
||||||
|
|
||||||
// OIData Open Interest数据
|
|
||||||
type OIData struct {
|
|
||||||
Latest float64
|
|
||||||
Average float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// IntradayData 日内数据(3分钟间隔) - 主要用于获取实时价格和放量分析
|
|
||||||
type IntradayData struct {
|
|
||||||
MidPrices []float64
|
|
||||||
EMA20Values []float64
|
|
||||||
MACDValues []float64
|
|
||||||
RSI7Values []float64
|
|
||||||
RSI14Values []float64
|
|
||||||
Volumes []float64 // 成交量序列(用于放量检测)
|
|
||||||
BuySellRatios []float64 // 买卖压力比序列(>0.6多方强,<0.4空方强)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MidTermData15m 15分钟时间框架数据 - 短期趋势过滤
|
|
||||||
type MidTermData15m struct {
|
|
||||||
MidPrices []float64
|
|
||||||
EMA20Values []float64
|
|
||||||
MACDValues []float64
|
|
||||||
RSI7Values []float64
|
|
||||||
RSI14Values []float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// MidTermData1h 1小时时间框架数据 - 中期趋势确认
|
|
||||||
type MidTermData1h struct {
|
|
||||||
MidPrices []float64
|
|
||||||
EMA20Values []float64
|
|
||||||
MACDValues []float64
|
|
||||||
RSI7Values []float64
|
|
||||||
RSI14Values []float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// LongerTermData 长期数据(4小时时间框架)
|
|
||||||
type LongerTermData struct {
|
|
||||||
EMA20 float64
|
|
||||||
EMA50 float64
|
|
||||||
ATR3 float64
|
|
||||||
ATR14 float64
|
|
||||||
CurrentVolume float64
|
|
||||||
AverageVolume float64
|
|
||||||
MACDValues []float64
|
|
||||||
RSI14Values []float64
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kline K线数据
|
|
||||||
type Kline struct {
|
|
||||||
OpenTime int64
|
|
||||||
Open float64
|
|
||||||
High float64
|
|
||||||
Low float64
|
|
||||||
Close float64
|
|
||||||
Volume float64
|
|
||||||
CloseTime int64
|
|
||||||
TakerBuyVolume float64 // 主动买入量(多方力量)
|
|
||||||
BuySellRatio float64 // 买卖压力比 = TakerBuyVolume / Volume
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取指定代币的市场数据
|
// Get 获取指定代币的市场数据
|
||||||
func Get(symbol string) (*Data, error) {
|
func Get(symbol string) (*Data, error) {
|
||||||
|
var klines3m, klines4h []Kline
|
||||||
|
var err error
|
||||||
// 标准化symbol
|
// 标准化symbol
|
||||||
symbol = Normalize(symbol)
|
symbol = Normalize(symbol)
|
||||||
|
// 获取3分钟K线数据 (最近10个)
|
||||||
// 获取3分钟K线数据 (最近10个) - 用于实时价格
|
klines3m, err = WSMonitorCli.GetCurrentKlines(symbol, "3m") // 多获取一些用于计算
|
||||||
klines3m, err := getKlines(symbol, "3m", 40) // 多获取一些用于计算
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取3分钟K线失败: %v", err)
|
return nil, fmt.Errorf("获取3分钟K线失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取15分钟K线数据 (最近10个) - 短期趋势
|
// 获取4小时K线数据 (最近10个)
|
||||||
klines15m, err := getKlines(symbol, "15m", 40)
|
klines4h, err = WSMonitorCli.GetCurrentKlines(symbol, "4h") // 多获取用于计算指标
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("获取15分钟K线失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取1小时K线数据 (最近10个) - 中期趋势
|
|
||||||
klines1h, err := getKlines(symbol, "1h", 60)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("获取1小时K线失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取4小时K线数据 (最近10个) - 长期趋势
|
|
||||||
klines4h, err := getKlines(symbol, "4h", 60) // 多获取用于计算指标
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取4小时K线失败: %v", err)
|
return nil, fmt.Errorf("获取4小时K线失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -151,16 +63,10 @@ func Get(symbol string) (*Data, error) {
|
|||||||
// 获取Funding Rate
|
// 获取Funding Rate
|
||||||
fundingRate, _ := getFundingRate(symbol)
|
fundingRate, _ := getFundingRate(symbol)
|
||||||
|
|
||||||
// 计算日内系列数据 (3分钟)
|
// 计算日内系列数据
|
||||||
intradayData := calculateIntradaySeries(klines3m)
|
intradayData := calculateIntradaySeries(klines3m)
|
||||||
|
|
||||||
// 计算15分钟系列数据
|
// 计算长期数据
|
||||||
midTermData15m := calculateMidTermSeries15m(klines15m)
|
|
||||||
|
|
||||||
// 计算1小时系列数据
|
|
||||||
midTermData1h := calculateMidTermSeries1h(klines1h)
|
|
||||||
|
|
||||||
// 计算长期数据 (4小时)
|
|
||||||
longerTermData := calculateLongerTermData(klines4h)
|
longerTermData := calculateLongerTermData(klines4h)
|
||||||
|
|
||||||
return &Data{
|
return &Data{
|
||||||
@@ -174,66 +80,10 @@ func Get(symbol string) (*Data, error) {
|
|||||||
OpenInterest: oiData,
|
OpenInterest: oiData,
|
||||||
FundingRate: fundingRate,
|
FundingRate: fundingRate,
|
||||||
IntradaySeries: intradayData,
|
IntradaySeries: intradayData,
|
||||||
MidTermSeries15m: midTermData15m,
|
|
||||||
MidTermSeries1h: midTermData1h,
|
|
||||||
LongerTermContext: longerTermData,
|
LongerTermContext: longerTermData,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getKlines 从Binance获取K线数据
|
|
||||||
func getKlines(symbol, interval string, limit int) ([]Kline, error) {
|
|
||||||
url := fmt.Sprintf("https://fapi.binance.com/fapi/v1/klines?symbol=%s&interval=%s&limit=%d",
|
|
||||||
symbol, interval, limit)
|
|
||||||
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var rawData [][]interface{}
|
|
||||||
if err := json.Unmarshal(body, &rawData); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
klines := make([]Kline, len(rawData))
|
|
||||||
for i, item := range rawData {
|
|
||||||
openTime := int64(item[0].(float64))
|
|
||||||
open, _ := parseFloat(item[1])
|
|
||||||
high, _ := parseFloat(item[2])
|
|
||||||
low, _ := parseFloat(item[3])
|
|
||||||
close, _ := parseFloat(item[4])
|
|
||||||
volume, _ := parseFloat(item[5])
|
|
||||||
closeTime := int64(item[6].(float64))
|
|
||||||
takerBuyVolume, _ := parseFloat(item[9]) // 主动买入量
|
|
||||||
|
|
||||||
// 计算买卖压力比
|
|
||||||
buySellRatio := 0.0
|
|
||||||
if volume > 0 {
|
|
||||||
buySellRatio = takerBuyVolume / volume
|
|
||||||
}
|
|
||||||
|
|
||||||
klines[i] = Kline{
|
|
||||||
OpenTime: openTime,
|
|
||||||
Open: open,
|
|
||||||
High: high,
|
|
||||||
Low: low,
|
|
||||||
Close: close,
|
|
||||||
Volume: volume,
|
|
||||||
CloseTime: closeTime,
|
|
||||||
TakerBuyVolume: takerBuyVolume,
|
|
||||||
BuySellRatio: buySellRatio,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return klines, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateEMA 计算EMA
|
// calculateEMA 计算EMA
|
||||||
func calculateEMA(klines []Kline, period int) float64 {
|
func calculateEMA(klines []Kline, period int) float64 {
|
||||||
if len(klines) < period {
|
if len(klines) < period {
|
||||||
@@ -356,100 +206,6 @@ func calculateIntradaySeries(klines []Kline) *IntradayData {
|
|||||||
MACDValues: make([]float64, 0, 10),
|
MACDValues: make([]float64, 0, 10),
|
||||||
RSI7Values: make([]float64, 0, 10),
|
RSI7Values: make([]float64, 0, 10),
|
||||||
RSI14Values: make([]float64, 0, 10),
|
RSI14Values: make([]float64, 0, 10),
|
||||||
Volumes: make([]float64, 0, 10),
|
|
||||||
BuySellRatios: make([]float64, 0, 10),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取最近10个数据点
|
|
||||||
start := len(klines) - 10
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := start; i < len(klines); i++ {
|
|
||||||
data.MidPrices = append(data.MidPrices, klines[i].Close)
|
|
||||||
data.Volumes = append(data.Volumes, klines[i].Volume) // 成交量
|
|
||||||
data.BuySellRatios = append(data.BuySellRatios, klines[i].BuySellRatio) // 买卖压力比
|
|
||||||
|
|
||||||
// 计算每个点的EMA20
|
|
||||||
if i >= 19 {
|
|
||||||
ema20 := calculateEMA(klines[:i+1], 20)
|
|
||||||
data.EMA20Values = append(data.EMA20Values, ema20)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算每个点的MACD
|
|
||||||
if i >= 25 {
|
|
||||||
macd := calculateMACD(klines[:i+1])
|
|
||||||
data.MACDValues = append(data.MACDValues, macd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算每个点的RSI
|
|
||||||
if i >= 7 {
|
|
||||||
rsi7 := calculateRSI(klines[:i+1], 7)
|
|
||||||
data.RSI7Values = append(data.RSI7Values, rsi7)
|
|
||||||
}
|
|
||||||
if i >= 14 {
|
|
||||||
rsi14 := calculateRSI(klines[:i+1], 14)
|
|
||||||
data.RSI14Values = append(data.RSI14Values, rsi14)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateMidTermSeries15m 计算15分钟系列数据
|
|
||||||
func calculateMidTermSeries15m(klines []Kline) *MidTermData15m {
|
|
||||||
data := &MidTermData15m{
|
|
||||||
MidPrices: make([]float64, 0, 10),
|
|
||||||
EMA20Values: make([]float64, 0, 10),
|
|
||||||
MACDValues: make([]float64, 0, 10),
|
|
||||||
RSI7Values: make([]float64, 0, 10),
|
|
||||||
RSI14Values: make([]float64, 0, 10),
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取最近10个数据点
|
|
||||||
start := len(klines) - 10
|
|
||||||
if start < 0 {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := start; i < len(klines); i++ {
|
|
||||||
data.MidPrices = append(data.MidPrices, klines[i].Close)
|
|
||||||
|
|
||||||
// 计算每个点的EMA20
|
|
||||||
if i >= 19 {
|
|
||||||
ema20 := calculateEMA(klines[:i+1], 20)
|
|
||||||
data.EMA20Values = append(data.EMA20Values, ema20)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算每个点的MACD
|
|
||||||
if i >= 25 {
|
|
||||||
macd := calculateMACD(klines[:i+1])
|
|
||||||
data.MACDValues = append(data.MACDValues, macd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算每个点的RSI
|
|
||||||
if i >= 7 {
|
|
||||||
rsi7 := calculateRSI(klines[:i+1], 7)
|
|
||||||
data.RSI7Values = append(data.RSI7Values, rsi7)
|
|
||||||
}
|
|
||||||
if i >= 14 {
|
|
||||||
rsi14 := calculateRSI(klines[:i+1], 14)
|
|
||||||
data.RSI14Values = append(data.RSI14Values, rsi14)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateMidTermSeries1h 计算1小时系列数据
|
|
||||||
func calculateMidTermSeries1h(klines []Kline) *MidTermData1h {
|
|
||||||
data := &MidTermData1h{
|
|
||||||
MidPrices: make([]float64, 0, 10),
|
|
||||||
EMA20Values: make([]float64, 0, 10),
|
|
||||||
MACDValues: make([]float64, 0, 10),
|
|
||||||
RSI7Values: make([]float64, 0, 10),
|
|
||||||
RSI14Values: make([]float64, 0, 10),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取最近10个数据点
|
// 获取最近10个数据点
|
||||||
@@ -640,54 +396,6 @@ func Format(data *Data) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.MidTermSeries15m != nil {
|
|
||||||
sb.WriteString("Mid‑term series (15‑minute intervals, oldest → latest):\n\n")
|
|
||||||
|
|
||||||
if len(data.MidTermSeries15m.MidPrices) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidTermSeries15m.MidPrices)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries15m.EMA20Values) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("EMA indicators (20‑period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.EMA20Values)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries15m.MACDValues) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MidTermSeries15m.MACDValues)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries15m.RSI7Values) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("RSI indicators (7‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.RSI7Values)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries15m.RSI14Values) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("RSI indicators (14‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.RSI14Values)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.MidTermSeries1h != nil {
|
|
||||||
sb.WriteString("Mid‑term series (1‑hour intervals, oldest → latest):\n\n")
|
|
||||||
|
|
||||||
if len(data.MidTermSeries1h.MidPrices) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidTermSeries1h.MidPrices)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries1h.EMA20Values) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("EMA indicators (20‑period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.EMA20Values)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries1h.MACDValues) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MidTermSeries1h.MACDValues)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries1h.RSI7Values) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("RSI indicators (7‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.RSI7Values)))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(data.MidTermSeries1h.RSI14Values) > 0 {
|
|
||||||
sb.WriteString(fmt.Sprintf("RSI indicators (14‑Period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.RSI14Values)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.LongerTermContext != nil {
|
if data.LongerTermContext != nil {
|
||||||
sb.WriteString("Longer‑term context (4‑hour timeframe):\n\n")
|
sb.WriteString("Longer‑term context (4‑hour timeframe):\n\n")
|
||||||
|
|
||||||
|
|||||||
260
market/monitor.go
Normal file
260
market/monitor.go
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WSMonitor struct {
|
||||||
|
wsClient *WSClient
|
||||||
|
combinedClient *CombinedStreamsClient
|
||||||
|
symbols []string
|
||||||
|
featuresMap sync.Map
|
||||||
|
alertsChan chan Alert
|
||||||
|
klineDataMap3m sync.Map // 存储每个交易对的K线历史数据
|
||||||
|
klineDataMap4h sync.Map // 存储每个交易对的K线历史数据
|
||||||
|
tickerDataMap sync.Map // 存储每个交易对的ticker数据
|
||||||
|
batchSize int
|
||||||
|
filterSymbols sync.Map // 使用sync.Map来存储需要监控的币种和其状态
|
||||||
|
symbolStats sync.Map // 存储币种统计信息
|
||||||
|
FilterSymbol []string //经过筛选的币种
|
||||||
|
}
|
||||||
|
type SymbolStats struct {
|
||||||
|
LastActiveTime time.Time
|
||||||
|
AlertCount int
|
||||||
|
VolumeSpikeCount int
|
||||||
|
LastAlertTime time.Time
|
||||||
|
Score float64 // 综合评分
|
||||||
|
}
|
||||||
|
|
||||||
|
var WSMonitorCli *WSMonitor
|
||||||
|
var subKlineTime = []string{"3m", "4h"} // 管理订阅流的K线周期
|
||||||
|
|
||||||
|
func NewWSMonitor(batchSize int) *WSMonitor {
|
||||||
|
WSMonitorCli = &WSMonitor{
|
||||||
|
wsClient: NewWSClient(),
|
||||||
|
combinedClient: NewCombinedStreamsClient(batchSize),
|
||||||
|
alertsChan: make(chan Alert, 1000),
|
||||||
|
batchSize: batchSize,
|
||||||
|
}
|
||||||
|
return WSMonitorCli
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) Initialize(coins []string) error {
|
||||||
|
log.Println("初始化WebSocket监控器...")
|
||||||
|
// 获取交易对信息
|
||||||
|
apiClient := NewAPIClient()
|
||||||
|
// 如果不指定交易对,则使用market市场的所有交易对币种
|
||||||
|
if len(coins) == 0 {
|
||||||
|
exchangeInfo, err := apiClient.GetExchangeInfo()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// 筛选永续合约交易对 --仅测试时使用
|
||||||
|
//exchangeInfo.Symbols = exchangeInfo.Symbols[0:2]
|
||||||
|
for _, symbol := range exchangeInfo.Symbols {
|
||||||
|
if symbol.Status == "TRADING" && symbol.ContractType == "PERPETUAL" && strings.ToUpper(symbol.Symbol[len(symbol.Symbol)-4:]) == "USDT" {
|
||||||
|
m.symbols = append(m.symbols, symbol.Symbol)
|
||||||
|
m.filterSymbols.Store(symbol.Symbol, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.symbols = coins
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("找到 %d 个交易对", len(m.symbols))
|
||||||
|
// 初始化历史数据
|
||||||
|
if err := m.initializeHistoricalData(); err != nil {
|
||||||
|
log.Printf("初始化历史数据失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) initializeHistoricalData() error {
|
||||||
|
apiClient := NewAPIClient()
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
semaphore := make(chan struct{}, 5) // 限制并发数
|
||||||
|
|
||||||
|
for _, symbol := range m.symbols {
|
||||||
|
wg.Add(1)
|
||||||
|
semaphore <- struct{}{}
|
||||||
|
|
||||||
|
go func(s string) {
|
||||||
|
defer wg.Done()
|
||||||
|
defer func() { <-semaphore }()
|
||||||
|
|
||||||
|
// 获取历史K线数据
|
||||||
|
klines, err := apiClient.GetKlines(s, "3m", 100)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取 %s 历史数据失败: %v", s, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(klines) > 0 {
|
||||||
|
m.klineDataMap3m.Store(s, klines)
|
||||||
|
log.Printf("已加载 %s 的历史K线数据-3m: %d 条", s, len(klines))
|
||||||
|
}
|
||||||
|
// 获取历史K线数据
|
||||||
|
klines4h, err := apiClient.GetKlines(s, "4h", 100)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("获取 %s 历史数据失败: %v", s, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(klines4h) > 0 {
|
||||||
|
m.klineDataMap4h.Store(s, klines)
|
||||||
|
log.Printf("已加载 %s 的历史K线数据-4h: %d 条", s, len(klines))
|
||||||
|
}
|
||||||
|
}(symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) Start(coins []string) {
|
||||||
|
log.Printf("启动WebSocket实时监控...")
|
||||||
|
// 初始化交易对
|
||||||
|
err := m.Initialize(coins)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ 初始化币种: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.combinedClient.Connect()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ 批量订阅流: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 订阅所有交易对
|
||||||
|
err = m.subscribeAll()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ 订阅币种交易对: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribeSymbol 注册监听
|
||||||
|
func (m *WSMonitor) subscribeSymbol(symbol, st string) []string {
|
||||||
|
var streams []string
|
||||||
|
stream := fmt.Sprintf("%s@kline_%s", strings.ToLower(symbol), st)
|
||||||
|
ch := m.combinedClient.AddSubscriber(stream, 100)
|
||||||
|
streams = append(streams, stream)
|
||||||
|
go m.handleKlineData(symbol, ch, st)
|
||||||
|
|
||||||
|
return streams
|
||||||
|
}
|
||||||
|
func (m *WSMonitor) subscribeAll() error {
|
||||||
|
// 执行批量订阅
|
||||||
|
log.Println("开始订阅所有交易对...")
|
||||||
|
for _, symbol := range m.symbols {
|
||||||
|
for _, st := range subKlineTime {
|
||||||
|
m.subscribeSymbol(symbol, st)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, st := range subKlineTime {
|
||||||
|
err := m.combinedClient.BatchSubscribeKlines(m.symbols, st)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("❌ 订阅3m K线: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println("所有交易对订阅完成")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) handleKlineData(symbol string, ch <-chan []byte, _time string) {
|
||||||
|
for data := range ch {
|
||||||
|
var klineData KlineWSData
|
||||||
|
if err := json.Unmarshal(data, &klineData); err != nil {
|
||||||
|
log.Printf("解析Kline数据失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.processKlineUpdate(symbol, klineData, _time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) getKlineDataMap(_time string) *sync.Map {
|
||||||
|
var klineDataMap *sync.Map
|
||||||
|
if _time == "3m" {
|
||||||
|
klineDataMap = &m.klineDataMap3m
|
||||||
|
} else if _time == "4h" {
|
||||||
|
klineDataMap = &m.klineDataMap4h
|
||||||
|
} else {
|
||||||
|
klineDataMap = &sync.Map{}
|
||||||
|
}
|
||||||
|
return klineDataMap
|
||||||
|
}
|
||||||
|
func (m *WSMonitor) processKlineUpdate(symbol string, wsData KlineWSData, _time string) {
|
||||||
|
// 转换WebSocket数据为Kline结构
|
||||||
|
kline := Kline{
|
||||||
|
OpenTime: wsData.Kline.StartTime,
|
||||||
|
CloseTime: wsData.Kline.CloseTime,
|
||||||
|
Trades: wsData.Kline.NumberOfTrades,
|
||||||
|
}
|
||||||
|
kline.Open, _ = parseFloat(wsData.Kline.OpenPrice)
|
||||||
|
kline.High, _ = parseFloat(wsData.Kline.HighPrice)
|
||||||
|
kline.Low, _ = parseFloat(wsData.Kline.LowPrice)
|
||||||
|
kline.Close, _ = parseFloat(wsData.Kline.ClosePrice)
|
||||||
|
kline.Volume, _ = parseFloat(wsData.Kline.Volume)
|
||||||
|
kline.High, _ = parseFloat(wsData.Kline.HighPrice)
|
||||||
|
kline.QuoteVolume, _ = parseFloat(wsData.Kline.QuoteVolume)
|
||||||
|
kline.TakerBuyBaseVolume, _ = parseFloat(wsData.Kline.TakerBuyBaseVolume)
|
||||||
|
kline.TakerBuyQuoteVolume, _ = parseFloat(wsData.Kline.TakerBuyQuoteVolume)
|
||||||
|
// 更新K线数据
|
||||||
|
var klineDataMap = m.getKlineDataMap(_time)
|
||||||
|
value, exists := klineDataMap.Load(symbol)
|
||||||
|
var klines []Kline
|
||||||
|
if exists {
|
||||||
|
klines = value.([]Kline)
|
||||||
|
|
||||||
|
// 检查是否是新的K线
|
||||||
|
if len(klines) > 0 && klines[len(klines)-1].OpenTime == kline.OpenTime {
|
||||||
|
// 更新当前K线
|
||||||
|
klines[len(klines)-1] = kline
|
||||||
|
} else {
|
||||||
|
// 添加新K线
|
||||||
|
klines = append(klines, kline)
|
||||||
|
|
||||||
|
// 保持数据长度
|
||||||
|
if len(klines) > 100 {
|
||||||
|
klines = klines[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
klines = []Kline{kline}
|
||||||
|
}
|
||||||
|
|
||||||
|
klineDataMap.Store(symbol, klines)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) GetCurrentKlines(symbol string, _time string) ([]Kline, error) {
|
||||||
|
// 对每一个进来的symbol检测是否存在内类 是否的话就订阅它
|
||||||
|
value, exists := m.getKlineDataMap(_time).Load(symbol)
|
||||||
|
if !exists {
|
||||||
|
// 如果Ws数据未初始化完成时,单独使用api获取 - 兼容性代码 (防止在未初始化完成是,已经有交易员运行)
|
||||||
|
apiClient := NewAPIClient()
|
||||||
|
klines, err := apiClient.GetKlines(symbol, _time, 100)
|
||||||
|
m.getKlineDataMap(_time).Store(strings.ToUpper(symbol), klines) //动态缓存进缓存
|
||||||
|
subStr := m.subscribeSymbol(symbol, _time)
|
||||||
|
subErr := m.combinedClient.subscribeStreams(subStr)
|
||||||
|
log.Printf("动态订阅流: %v", subStr)
|
||||||
|
if subErr != nil {
|
||||||
|
return nil, fmt.Errorf("动态订阅%v分钟K线失败: %v", _time, subErr)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取%v分钟K线失败: %v", _time, err)
|
||||||
|
}
|
||||||
|
return klines, fmt.Errorf("symbol不存在")
|
||||||
|
}
|
||||||
|
return value.([]Kline), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WSMonitor) Close() {
|
||||||
|
m.wsClient.Close()
|
||||||
|
close(m.alertsChan)
|
||||||
|
}
|
||||||
157
market/types.go
Normal file
157
market/types.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Data 市场数据结构
|
||||||
|
type Data struct {
|
||||||
|
Symbol string
|
||||||
|
CurrentPrice float64
|
||||||
|
PriceChange1h float64 // 1小时价格变化百分比
|
||||||
|
PriceChange4h float64 // 4小时价格变化百分比
|
||||||
|
CurrentEMA20 float64
|
||||||
|
CurrentMACD float64
|
||||||
|
CurrentRSI7 float64
|
||||||
|
OpenInterest *OIData
|
||||||
|
FundingRate float64
|
||||||
|
IntradaySeries *IntradayData
|
||||||
|
LongerTermContext *LongerTermData
|
||||||
|
}
|
||||||
|
|
||||||
|
// OIData Open Interest数据
|
||||||
|
type OIData struct {
|
||||||
|
Latest float64
|
||||||
|
Average float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntradayData 日内数据(3分钟间隔)
|
||||||
|
type IntradayData struct {
|
||||||
|
MidPrices []float64
|
||||||
|
EMA20Values []float64
|
||||||
|
MACDValues []float64
|
||||||
|
RSI7Values []float64
|
||||||
|
RSI14Values []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongerTermData 长期数据(4小时时间框架)
|
||||||
|
type LongerTermData struct {
|
||||||
|
EMA20 float64
|
||||||
|
EMA50 float64
|
||||||
|
ATR3 float64
|
||||||
|
ATR14 float64
|
||||||
|
CurrentVolume float64
|
||||||
|
AverageVolume float64
|
||||||
|
MACDValues []float64
|
||||||
|
RSI14Values []float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binance API 响应结构
|
||||||
|
type ExchangeInfo struct {
|
||||||
|
Symbols []SymbolInfo `json:"symbols"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolInfo struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
BaseAsset string `json:"baseAsset"`
|
||||||
|
QuoteAsset string `json:"quoteAsset"`
|
||||||
|
ContractType string `json:"contractType"`
|
||||||
|
PricePrecision int `json:"pricePrecision"`
|
||||||
|
QuantityPrecision int `json:"quantityPrecision"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Kline struct {
|
||||||
|
OpenTime int64 `json:"openTime"`
|
||||||
|
Open float64 `json:"open"`
|
||||||
|
High float64 `json:"high"`
|
||||||
|
Low float64 `json:"low"`
|
||||||
|
Close float64 `json:"close"`
|
||||||
|
Volume float64 `json:"volume"`
|
||||||
|
CloseTime int64 `json:"closeTime"`
|
||||||
|
QuoteVolume float64 `json:"quoteVolume"`
|
||||||
|
Trades int `json:"trades"`
|
||||||
|
TakerBuyBaseVolume float64 `json:"takerBuyBaseVolume"`
|
||||||
|
TakerBuyQuoteVolume float64 `json:"takerBuyQuoteVolume"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KlineResponse []interface{}
|
||||||
|
|
||||||
|
type PriceTicker struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Price string `json:"price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Ticker24hr struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
PriceChange string `json:"priceChange"`
|
||||||
|
PriceChangePercent string `json:"priceChangePercent"`
|
||||||
|
Volume string `json:"volume"`
|
||||||
|
QuoteVolume string `json:"quoteVolume"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特征数据结构
|
||||||
|
type SymbolFeatures struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
PriceChange15Min float64 `json:"price_change_15min"`
|
||||||
|
PriceChange1H float64 `json:"price_change_1h"`
|
||||||
|
PriceChange4H float64 `json:"price_change_4h"`
|
||||||
|
Volume float64 `json:"volume"`
|
||||||
|
VolumeRatio5 float64 `json:"volume_ratio_5"`
|
||||||
|
VolumeRatio20 float64 `json:"volume_ratio_20"`
|
||||||
|
VolumeTrend float64 `json:"volume_trend"`
|
||||||
|
RSI14 float64 `json:"rsi_14"`
|
||||||
|
SMA5 float64 `json:"sma_5"`
|
||||||
|
SMA10 float64 `json:"sma_10"`
|
||||||
|
SMA20 float64 `json:"sma_20"`
|
||||||
|
HighLowRatio float64 `json:"high_low_ratio"`
|
||||||
|
Volatility20 float64 `json:"volatility_20"`
|
||||||
|
PositionInRange float64 `json:"position_in_range"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 警报数据结构
|
||||||
|
type Alert struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Value float64 `json:"value"`
|
||||||
|
Threshold float64 `json:"threshold"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AlertThresholds AlertThresholds `json:"alert_thresholds"`
|
||||||
|
UpdateInterval int `json:"update_interval"` // seconds
|
||||||
|
CleanupConfig CleanupConfig `json:"cleanup_config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertThresholds struct {
|
||||||
|
VolumeSpike float64 `json:"volume_spike"`
|
||||||
|
PriceChange15Min float64 `json:"price_change_15min"`
|
||||||
|
VolumeTrend float64 `json:"volume_trend"`
|
||||||
|
RSIOverbought float64 `json:"rsi_overbought"`
|
||||||
|
RSIOversold float64 `json:"rsi_oversold"`
|
||||||
|
}
|
||||||
|
type CleanupConfig struct {
|
||||||
|
InactiveTimeout time.Duration `json:"inactive_timeout"` // 不活跃超时时间
|
||||||
|
MinScoreThreshold float64 `json:"min_score_threshold"` // 最低评分阈值
|
||||||
|
NoAlertTimeout time.Duration `json:"no_alert_timeout"` // 无警报超时时间
|
||||||
|
CheckInterval time.Duration `json:"check_interval"` // 检查间隔
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = Config{
|
||||||
|
AlertThresholds: AlertThresholds{
|
||||||
|
VolumeSpike: 3.0,
|
||||||
|
PriceChange15Min: 0.05,
|
||||||
|
VolumeTrend: 2.0,
|
||||||
|
RSIOverbought: 70,
|
||||||
|
RSIOversold: 30,
|
||||||
|
},
|
||||||
|
CleanupConfig: CleanupConfig{
|
||||||
|
InactiveTimeout: 30 * time.Minute,
|
||||||
|
MinScoreThreshold: 15.0,
|
||||||
|
NoAlertTimeout: 20 * time.Minute,
|
||||||
|
CheckInterval: 5 * time.Minute,
|
||||||
|
},
|
||||||
|
UpdateInterval: 60, // 1 minute
|
||||||
|
}
|
||||||
231
market/websocket_client.go
Normal file
231
market/websocket_client.go
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
package market
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WSClient struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
mu sync.RWMutex
|
||||||
|
subscribers map[string]chan []byte
|
||||||
|
reconnect bool
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type WSMessage struct {
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type KlineWSData struct {
|
||||||
|
EventType string `json:"e"`
|
||||||
|
EventTime int64 `json:"E"`
|
||||||
|
Symbol string `json:"s"`
|
||||||
|
Kline struct {
|
||||||
|
StartTime int64 `json:"t"`
|
||||||
|
CloseTime int64 `json:"T"`
|
||||||
|
Symbol string `json:"s"`
|
||||||
|
Interval string `json:"i"`
|
||||||
|
FirstTradeID int64 `json:"f"`
|
||||||
|
LastTradeID int64 `json:"L"`
|
||||||
|
OpenPrice string `json:"o"`
|
||||||
|
ClosePrice string `json:"c"`
|
||||||
|
HighPrice string `json:"h"`
|
||||||
|
LowPrice string `json:"l"`
|
||||||
|
Volume string `json:"v"`
|
||||||
|
NumberOfTrades int `json:"n"`
|
||||||
|
IsFinal bool `json:"x"`
|
||||||
|
QuoteVolume string `json:"q"`
|
||||||
|
TakerBuyBaseVolume string `json:"V"`
|
||||||
|
TakerBuyQuoteVolume string `json:"Q"`
|
||||||
|
} `json:"k"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TickerWSData struct {
|
||||||
|
EventType string `json:"e"`
|
||||||
|
EventTime int64 `json:"E"`
|
||||||
|
Symbol string `json:"s"`
|
||||||
|
PriceChange string `json:"p"`
|
||||||
|
PriceChangePercent string `json:"P"`
|
||||||
|
WeightedAvgPrice string `json:"w"`
|
||||||
|
LastPrice string `json:"c"`
|
||||||
|
LastQty string `json:"Q"`
|
||||||
|
OpenPrice string `json:"o"`
|
||||||
|
HighPrice string `json:"h"`
|
||||||
|
LowPrice string `json:"l"`
|
||||||
|
Volume string `json:"v"`
|
||||||
|
QuoteVolume string `json:"q"`
|
||||||
|
OpenTime int64 `json:"O"`
|
||||||
|
CloseTime int64 `json:"C"`
|
||||||
|
FirstID int64 `json:"F"`
|
||||||
|
LastID int64 `json:"L"`
|
||||||
|
Count int `json:"n"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWSClient() *WSClient {
|
||||||
|
return &WSClient{
|
||||||
|
subscribers: make(map[string]chan []byte),
|
||||||
|
reconnect: true,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) Connect() error {
|
||||||
|
dialer := websocket.Dialer{
|
||||||
|
HandshakeTimeout: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, _, err := dialer.Dial("wss://ws-fapi.binance.com/ws-fapi/v1", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("WebSocket连接失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.conn = conn
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
log.Println("WebSocket连接成功")
|
||||||
|
|
||||||
|
// 启动消息读取循环
|
||||||
|
go w.readMessages()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) SubscribeKline(symbol, interval string) error {
|
||||||
|
stream := fmt.Sprintf("%s@kline_%s", symbol, interval)
|
||||||
|
return w.subscribe(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) SubscribeTicker(symbol string) error {
|
||||||
|
stream := fmt.Sprintf("%s@ticker", symbol)
|
||||||
|
return w.subscribe(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) SubscribeMiniTicker(symbol string) error {
|
||||||
|
stream := fmt.Sprintf("%s@miniTicker", symbol)
|
||||||
|
return w.subscribe(stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) subscribe(stream string) error {
|
||||||
|
subscribeMsg := map[string]interface{}{
|
||||||
|
"method": "SUBSCRIBE",
|
||||||
|
"params": []string{stream},
|
||||||
|
"id": time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
|
||||||
|
if w.conn == nil {
|
||||||
|
return fmt.Errorf("WebSocket未连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.conn.WriteJSON(subscribeMsg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("订阅流: %s", stream)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) readMessages() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
w.mu.RLock()
|
||||||
|
conn := w.conn
|
||||||
|
w.mu.RUnlock()
|
||||||
|
|
||||||
|
if conn == nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, message, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("读取WebSocket消息失败: %v", err)
|
||||||
|
w.handleReconnect()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.handleMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) handleMessage(message []byte) {
|
||||||
|
var wsMsg WSMessage
|
||||||
|
if err := json.Unmarshal(message, &wsMsg); err != nil {
|
||||||
|
// 可能是其他格式的消息
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.RLock()
|
||||||
|
ch, exists := w.subscribers[wsMsg.Stream]
|
||||||
|
w.mu.RUnlock()
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
select {
|
||||||
|
case ch <- wsMsg.Data:
|
||||||
|
default:
|
||||||
|
log.Printf("订阅者通道已满: %s", wsMsg.Stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) handleReconnect() {
|
||||||
|
if !w.reconnect {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("尝试重新连接...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
if err := w.Connect(); err != nil {
|
||||||
|
log.Printf("重新连接失败: %v", err)
|
||||||
|
go w.handleReconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) AddSubscriber(stream string, bufferSize int) <-chan []byte {
|
||||||
|
ch := make(chan []byte, bufferSize)
|
||||||
|
w.mu.Lock()
|
||||||
|
w.subscribers[stream] = ch
|
||||||
|
w.mu.Unlock()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) RemoveSubscriber(stream string) {
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.subscribers, stream)
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WSClient) Close() {
|
||||||
|
w.reconnect = false
|
||||||
|
close(w.done)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.conn != nil {
|
||||||
|
w.conn.Close()
|
||||||
|
w.conn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭所有订阅者通道
|
||||||
|
for stream, ch := range w.subscribers {
|
||||||
|
close(ch)
|
||||||
|
delete(w.subscribers, stream)
|
||||||
|
}
|
||||||
|
}
|
||||||
337
prompts/taro_long_prompts.txt
Normal file
337
prompts/taro_long_prompts.txt
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
|
||||||
|
|
||||||
|
## 🎯 核心分析哲学
|
||||||
|
**数据驱动决策** = 自主模式识别 × 多维度验证 × 动态风险评估 × 持续学习进化
|
||||||
|
|
||||||
|
📊 **分析自主权**:
|
||||||
|
- 自由组合所有可用技术指标
|
||||||
|
- 自主识别市场模式和趋势结构
|
||||||
|
- 动态构建交易逻辑和风控规则
|
||||||
|
- 实时评估机会质量和风险收益比
|
||||||
|
- 基于历史表现自主优化策略
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 主动止盈策略强化
|
||||||
|
### 核心问题认知
|
||||||
|
**当前主要问题**:开仓决策缺乏多周期趋势验证,常因局部波动信号误判导致反向建仓或陷入震荡。
|
||||||
|
**风险后果**:未确认多周期趋势一致性时盲目开仓,容易被短期反向波动洗出或错失主趋势行情。
|
||||||
|
|
||||||
|
### 多周期趋势确认 + 主动止盈规则
|
||||||
|
```
|
||||||
|
开仓前必须同时检查 3分钟、15分钟、1小时、4小时 的K线形态:
|
||||||
|
- 若四个周期中至少三个周期的结构方向一致(如均为上升通道或EMA20>EMA50),则可顺势开仓;
|
||||||
|
- 若短周期(3m,15m)出现反向形态,但中长周期(1h,4h)趋势强劲,可等待短周期修正后再进场;
|
||||||
|
- 若多周期趋势方向不一致(如15m上升但4h下降),必须等待趋势共振信号再开仓;
|
||||||
|
- 若任意周期出现顶部或底部反转形态(双顶、黄昏之星、锤头、吞没形态等),禁止盲目开仓。
|
||||||
|
|
||||||
|
止盈前需再次分析多周期K线形态以确认趋势:
|
||||||
|
- 若中长周期仍维持结构上升,可延长持仓时间;
|
||||||
|
- 若短周期出现反转或均线破位,应逐步止盈;
|
||||||
|
- 若量能放大但价格不创新高,代表动能衰减,应分批止盈锁定利润。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分级主动止盈规则
|
||||||
|
```
|
||||||
|
盈利状态下的强制止盈规则:
|
||||||
|
1. 盈利1-3%:重点保护,回撤50%立即止盈
|
||||||
|
2. 盈利3-5%:设置保本止损,回撤25%止盈
|
||||||
|
3. 盈利5-8%:移动止盈,回撤30%止盈
|
||||||
|
4. 盈利8-15%:让利润奔跑,但回撤30%必须止盈
|
||||||
|
5. 盈利>15%+:让利润奔跑,但回撤50%必须止盈
|
||||||
|
```
|
||||||
|
|
||||||
|
### 策略核心思想
|
||||||
|
开仓前必须验证多周期趋势一致性;顺势而为,不逆势操作。
|
||||||
|
止盈前必须重新分析多周期结构,趋势未破则让利润奔跑,一旦形态反转立即锁定收益。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💰 盈利状态的行为准则
|
||||||
|
### 盈利持仓的管理优先级
|
||||||
|
**你的首要任务**:管理好现有盈利持仓 > 寻找新机会
|
||||||
|
|
||||||
|
### 盈利状态下的决策流程
|
||||||
|
**分析持仓时的思维框架**:
|
||||||
|
```
|
||||||
|
对于每个持仓,按顺序思考:
|
||||||
|
1. 当前盈利多少?是否达到止盈标准?
|
||||||
|
2. 技术指标是否显示止盈信号?
|
||||||
|
3. 价格是否接近关键阻力/支撑?
|
||||||
|
4. 盈利是否开始回吐?回吐幅度如何?
|
||||||
|
5. 是否应该部分或全部止盈?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 学习进化与绩效分析
|
||||||
|
### 连续亏损记忆与分析
|
||||||
|
**当出现连续亏损时,你必须**:
|
||||||
|
1. **识别亏损模式**:分析亏损交易的共同特征
|
||||||
|
2. **诊断根本原因**:技术信号失效?市场环境变化?风控不当?
|
||||||
|
3. **制定改进措施**:调整信号筛选标准、优化仓位管理、改进止盈止损
|
||||||
|
4. **验证改进效果**:通过后续交易验证调整的有效性
|
||||||
|
|
||||||
|
**亏损分析框架**:
|
||||||
|
```
|
||||||
|
亏损原因分类:
|
||||||
|
- 技术信号失效(假突破、指标滞后)
|
||||||
|
- 市场环境突变(趋势转换、波动率剧变)
|
||||||
|
- 仓位管理不当(仓位过重、杠杆过高)
|
||||||
|
- 止盈止损设置不合理(过紧或过松)
|
||||||
|
- 交易频率过高(过度交易、情绪化决策)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 夏普比率深度分析
|
||||||
|
**基于夏普比率的策略调整**:
|
||||||
|
```
|
||||||
|
夏普比率 > 0.8(优秀):
|
||||||
|
- 保持当前策略框架
|
||||||
|
- 可适度增加高质量信号的风险暴露
|
||||||
|
- 继续优化止盈时机和仓位管理
|
||||||
|
|
||||||
|
夏普比率 0.3-0.8(良好):
|
||||||
|
- 维持标准风控措施
|
||||||
|
- 重点优化信号筛选质量
|
||||||
|
- 改进止盈策略,减少利润回吐
|
||||||
|
|
||||||
|
夏普比率 0-0.3(需改进):
|
||||||
|
- 收紧开仓标准,提高信心度门槛
|
||||||
|
- 降低单笔风险暴露(≤2%账户净值)
|
||||||
|
- 减少交易频率,专注高质量机会
|
||||||
|
- 重点分析近期亏损交易模式
|
||||||
|
|
||||||
|
夏普比率 < 0(防御模式):
|
||||||
|
- 停止新开仓,专注平仓管理
|
||||||
|
- 单笔风险暴露降至1%以下
|
||||||
|
- 深度分析所有亏损交易
|
||||||
|
- 连续观望至少3个周期(9分钟)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 交易频率控制机制
|
||||||
|
**严格避免高频交易**:
|
||||||
|
```
|
||||||
|
交易频率标准:
|
||||||
|
- 优秀交易员:每小时1-3笔交易
|
||||||
|
- 过度交易:每小时>10笔交易
|
||||||
|
- 最佳节奏:持仓时间30-120分钟
|
||||||
|
|
||||||
|
高频交易危害:
|
||||||
|
- 增加交易成本(手续费、滑点)
|
||||||
|
- 降低信号质量(冲动决策)
|
||||||
|
- 增加心理压力(情绪化交易)
|
||||||
|
- 降低夏普比率(收益波动增大)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 自主量化分析框架
|
||||||
|
### 可用数据维度(自由组合)
|
||||||
|
**📊 四个时间框架序列**(每个包含最近10个数据点):
|
||||||
|
1. **3分钟序列**:实时价格 + 放量分析(当前价格 = 最后一根K线的收盘价)
|
||||||
|
- Mid prices, EMA20, MACD, RSI7, RSI14
|
||||||
|
- **Volumes**: 成交量序列(用于检测放量)
|
||||||
|
- **BuySellRatios**: 买卖压力比(>0.6多方强,<0.4空方强)
|
||||||
|
2. **15分钟序列**:短期震荡区间识别(覆盖最近2.5小时)
|
||||||
|
- Mid prices, EMA20, MACD, RSI7, RSI14
|
||||||
|
3. **1小时序列**:中期支撑压力确认(覆盖最近10小时)
|
||||||
|
- Mid prices, EMA20, MACD, RSI7, RSI14
|
||||||
|
4. **4小时序列**:大趋势预警(覆盖最近40小时)
|
||||||
|
|
||||||
|
```
|
||||||
|
价格数据系列:
|
||||||
|
- 多时间框架K线(3m/15m/1h/4h)
|
||||||
|
- 当前价格、价格变化率(1h/4h)
|
||||||
|
- 最高价、最低价、开盘价、收盘价序列
|
||||||
|
|
||||||
|
趋势指标:
|
||||||
|
- EMA20(各时间框架)
|
||||||
|
- EMA50(4小时框架)
|
||||||
|
- MACD(快慢线、柱状图)
|
||||||
|
- 价格与EMA的相对位置
|
||||||
|
|
||||||
|
动量振荡器:
|
||||||
|
- RSI7(各时间框架)
|
||||||
|
- RSI14(各时间框架)
|
||||||
|
- 超买超卖区域识别
|
||||||
|
- 背离分析(价格与RSI)
|
||||||
|
|
||||||
|
成交量与资金流:
|
||||||
|
- **Volumes**: 成交量序列(用于检测放量)
|
||||||
|
- **BuySellRatios**: 买卖压力比(>0.6多方强,<0.4空方强)
|
||||||
|
- 成交量与价格走势的配合分析
|
||||||
|
- 资金流方向的实时判断
|
||||||
|
|
||||||
|
市场情绪数据:
|
||||||
|
- 持仓量(OI)变化及价值
|
||||||
|
- 资金费率(多空平衡)
|
||||||
|
- 成交量及变化模式
|
||||||
|
- 波动率特征(ATR)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📉 做空策略专项指导
|
||||||
|
### 做空信号识别标准
|
||||||
|
**你必须同等重视做空机会,当出现以下信号时积极考虑做空**:
|
||||||
|
|
||||||
|
**技术面做空信号**:
|
||||||
|
- EMA空头排列:价格<EMA20<EMA50
|
||||||
|
- MACD死叉且柱状图转负
|
||||||
|
- RSI从超买区域(>70)回落
|
||||||
|
- 价格跌破关键支撑位
|
||||||
|
- 上升趋势线被有效跌破
|
||||||
|
|
||||||
|
**量价关系做空信号**:
|
||||||
|
- 下跌时放量,反弹时缩量
|
||||||
|
- 买卖压力比持续<0.4
|
||||||
|
- 持仓量下降伴随价格下跌(资金流出)
|
||||||
|
- 大额爆仓数据显示空头占优
|
||||||
|
|
||||||
|
### 做空时机选择
|
||||||
|
**优先在以下时机开空仓**:
|
||||||
|
1. **反弹至阻力位**:价格反弹至前高或EMA阻力位
|
||||||
|
2. **趋势转换确认**:上升趋势明确转为下跌趋势
|
||||||
|
3. **技术指标共振**:多个时间框架同时出现做空信号
|
||||||
|
4. **市场情绪极端**:极度贪婪后的反转机会
|
||||||
|
|
||||||
|
### 自主模式识别能力
|
||||||
|
**你拥有完全自主权来识别以下模式**:
|
||||||
|
|
||||||
|
**趋势结构分析**:
|
||||||
|
- 自主判断趋势强度(弱/中/强/极强)
|
||||||
|
- 识别趋势启动/延续/衰竭信号
|
||||||
|
- 多时间框架趋势一致性评估
|
||||||
|
- 趋势线与通道的自主绘制
|
||||||
|
- 成交量与价格的方向配合
|
||||||
|
|
||||||
|
**震荡环境特征**:
|
||||||
|
- 价格在区间内运行
|
||||||
|
- EMA缠绕无明确方向
|
||||||
|
- 成交量萎缩或规律性波动
|
||||||
|
- 买卖压力比在中性区域
|
||||||
|
|
||||||
|
**转折环境特征**:
|
||||||
|
- 技术指标的多重背离
|
||||||
|
- 关键位置突破失败
|
||||||
|
- 成交量异常放大
|
||||||
|
- 市场情绪的极端化
|
||||||
|
|
||||||
|
### 环境适应性策略(自主构建)
|
||||||
|
**你基于识别到的市场环境自主制定策略**:
|
||||||
|
- 趋势市:顺势而为,让利润奔跑
|
||||||
|
- 震荡市:区间操作,及时止盈
|
||||||
|
- 转折市:谨慎观望,确认跟进
|
||||||
|
|
||||||
|
**下跌趋势结构分析**:
|
||||||
|
- 识别下跌趋势的强度和持续性
|
||||||
|
- 判断是回调还是趋势反转
|
||||||
|
- 分析下跌动量的衰竭信号
|
||||||
|
- 识别潜在的反弹阻力位
|
||||||
|
|
||||||
|
**做空环境特征**:
|
||||||
|
- 价格在关键阻力位受阻
|
||||||
|
- 技术指标出现顶背离
|
||||||
|
- 成交量在下跌时放大
|
||||||
|
- 市场情绪从极端乐观转向
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎚️ 自主风险评估体系
|
||||||
|
### 机会质量自主评估
|
||||||
|
**完全由你定义信号质量评分标准**:
|
||||||
|
- 技术面共振程度(0-40分)
|
||||||
|
- 量价配合情况(0-30分)
|
||||||
|
- 市场情绪验证(0-20分)
|
||||||
|
- 风险收益比评估(0-10分)
|
||||||
|
|
||||||
|
**信心度映射规则(自主定义)**:
|
||||||
|
- 90%+:多重确认+高盈亏比+明确趋势
|
||||||
|
- 80-89%:技术面共振+量价配合良好
|
||||||
|
- 70-79%:主要信号明确,但有轻微瑕疵
|
||||||
|
- <70%:信号不明确或风险过高
|
||||||
|
|
||||||
|
### 动态仓位配置
|
||||||
|
**基于自主风险评估的仓位管理**:
|
||||||
|
```
|
||||||
|
仓位配置 = f(信号质量, 市场波动率, 账户状态)
|
||||||
|
|
||||||
|
核心原则:
|
||||||
|
- 高质量信号 → 适当增加风险暴露
|
||||||
|
- 高波动环境 → 降低单笔风险
|
||||||
|
- 连续盈利 → 可适度激进
|
||||||
|
- 连续亏损 → 必须保守防御
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 自主止盈止损逻辑
|
||||||
|
### 动态止盈策略(完全自主)
|
||||||
|
**基于实时市场状况的止盈决策**:
|
||||||
|
- 趋势强度决定止盈宽松度
|
||||||
|
- 波动率环境调整回撤容忍度
|
||||||
|
- 技术指标提供具体止盈信号
|
||||||
|
- 持仓时间影响止盈紧迫性
|
||||||
|
|
||||||
|
**止盈触发条件(自主选择)**:
|
||||||
|
- 技术指标达到极端区域(RSI>85/<15)
|
||||||
|
- 出现明确的反转K线形态
|
||||||
|
- 量价背离或技术指标背离
|
||||||
|
- 达到关键阻力支撑位
|
||||||
|
- 盈利回撤超过动态阈值
|
||||||
|
|
||||||
|
### 智能止损设置
|
||||||
|
**基于技术分析的止损定位**:
|
||||||
|
- 关键支撑阻力位下方/上方
|
||||||
|
- 趋势结构破坏的确认点
|
||||||
|
- 波动率适应的合理距离
|
||||||
|
- 账户风险承受的硬约束
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧠 自主决策思维框架
|
||||||
|
### 分析流程(完全自主)
|
||||||
|
**你自主决定分析路径和重点**,按以下逻辑有序推进:
|
||||||
|
1. 绩效回顾:分析夏普比率和近期亏损模式,明确当前策略有效性。
|
||||||
|
2. 市场整体环境评估:判断市场处于趋势、震荡还是转折状态。
|
||||||
|
3. 持仓币种的独立技术分析:针对现有持仓单独拆解多周期信号。
|
||||||
|
4. 候选机会的多维度筛选:从技术面、量价等维度筛选新交易标的。
|
||||||
|
5. 风险收益比的自主计算:量化评估每笔交易的潜在风险与收益。
|
||||||
|
6. 仓位配置的合理性验证:结合账户状态与信号质量确认仓位。
|
||||||
|
|
||||||
|
### 机会评估标准(自主定义)
|
||||||
|
**你自主建立机会评估体系**,核心评估维度包括:
|
||||||
|
- 技术面确认度:多指标、多周期是否形成共振。
|
||||||
|
- 量价配合的健康程度:成交量与价格走势是否同向。
|
||||||
|
- 市场情绪的配合情况:资金流、持仓量等情绪数据是否支撑信号。
|
||||||
|
- 风险回报比的吸引力:潜在收益是否覆盖2倍以上潜在风险。
|
||||||
|
- 与现有持仓的相关性:避免新增高相关性持仓导致风险集中。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ 顶尖交易员思维
|
||||||
|
### 核心行为准则
|
||||||
|
**充分发挥你的分析能力**,严格遵循以下原则:
|
||||||
|
- ✅ 相信技术分析判断,包括明确的看跌信号。
|
||||||
|
- ✅ 同等重视做多和做空机会,不偏废任何方向。
|
||||||
|
- ✅ 在强势趋势中让利润奔跑,不轻易提前止盈。
|
||||||
|
- ✅ 动态调整策略适应市场变化,不墨守成规。
|
||||||
|
- ✅ 严格在风控边界内发挥创造性,不突破风险底线。
|
||||||
|
- ✅ 持续优化分析框架,基于历史表现迭代规则。
|
||||||
|
|
||||||
|
### 禁止行为清单
|
||||||
|
**严格避免以下行为,防止决策偏差**:
|
||||||
|
- ❌ 只做多不做空的单向偏见,忽视空头机会。
|
||||||
|
- ❌ 忽视明确的做空技术信号,导致错过反向收益。
|
||||||
|
- ❌ 在下跌趋势中逆势做多,对抗市场主趋势。
|
||||||
|
- ❌ 高频交易(每小时>10笔新开仓),增加成本与失误率。
|
||||||
|
- ❌ 忽视连续亏损的警示信号,不及时调整策略。
|
||||||
|
- ❌ 在夏普比率<0时强行交易,无视策略失效信号。
|
||||||
|
- ❌ 情绪化决策和报复性交易,被短期波动左右。
|
||||||
|
- ❌ 过度自信忽视风险控制,放宽开仓或仓位标准。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**核心提示**:你拥有完整的技术分析自主权,基于提供的多维数据自由构建交易逻辑。特别注意:震荡行情完全由你自主分析处理,我们不过多干预你的分析判断。
|
||||||
|
|
||||||
|
|
||||||
@@ -2,11 +2,22 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<!-- Google Tag Manager -->
|
||||||
|
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
|
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
|
||||||
|
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||||
|
})(window,document,'script','dataLayer','GTM-TM429527');</script>
|
||||||
|
<!-- End Google Tag Manager -->
|
||||||
<link rel="icon" type="image/svg+xml" href="/icons/nofx.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icons/nofx.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>NOFX - AI Auto Trading Dashboard</title>
|
<title>NOFX - AI Auto Trading Dashboard</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- Google Tag Manager (noscript) -->
|
||||||
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-TM429527"
|
||||||
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||||
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -27,16 +27,57 @@ function TestimonialCard({ quote, author, delay }: any) {
|
|||||||
|
|
||||||
export default function CommunitySection() {
|
export default function CommunitySection() {
|
||||||
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
|
const staggerContainer = { animate: { transition: { staggerChildren: 0.1 } } }
|
||||||
|
|
||||||
|
// 推特内容整合(保持原三列布局,超出自动换行)
|
||||||
|
const items: CardProps[] = [
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
'前不久非常火的 AI 量化交易系统 NOF1,在 GitHub 上有人将其复刻并开源,这就是 NOFX 项目。基于 DeepSeek、Qwen 等大语言模型,打造的通用架构 AI 交易操作系统,完成了从决策、到交易、再到复盘的闭环。GitHub: https://github.com/NoFxAiOS/nofx',
|
||||||
|
authorName: 'Michael Williams',
|
||||||
|
handle: '@MichaelWil93725',
|
||||||
|
avatarUrl:
|
||||||
|
'https://pbs.twimg.com/profile_images/1767615411594694659/Mj8Fdt6o_400x400.jpg',
|
||||||
|
tweetUrl:
|
||||||
|
'https://twitter.com/MichaelWil93725/status/1984980920395604008',
|
||||||
|
delay: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
'跑了一晚上 @nofx_ai 开源的 AI 自动交易,太有意思了,就看 AI 在那一会开空一会开多,一顿操作,虽然看不懂为什么,但是一晚上帮我赚了 6% 收益',
|
||||||
|
authorName: 'DIŸgöd',
|
||||||
|
handle: '@DIYgod',
|
||||||
|
avatarUrl:
|
||||||
|
'https://pbs.twimg.com/profile_images/1628393369029181440/r23HDDJk_400x400.jpg',
|
||||||
|
tweetUrl: 'https://twitter.com/DIYgod/status/1984442354515017923',
|
||||||
|
delay: 0.1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote:
|
||||||
|
'Open-source NOFX revives the legendary Alpha Arena, an AI-powered crypto futures battleground. Built on DeepSeek/Qwen AI, it trades live on Binance, Hyperliquid, and Aster DEX, featuring multi-AI battles and self-learning bots',
|
||||||
|
authorName: 'Kai',
|
||||||
|
handle: '@hqmank',
|
||||||
|
avatarUrl:
|
||||||
|
'https://pbs.twimg.com/profile_images/1905441261911506945/4YhLIqUm_400x400.jpg',
|
||||||
|
tweetUrl: 'https://twitter.com/hqmank/status/1984227431994290340',
|
||||||
|
delay: 0.15,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedSection>
|
<AnimatedSection>
|
||||||
<div className='max-w-7xl mx-auto'>
|
<div className='max-w-7xl mx-auto'>
|
||||||
<motion.div className='grid md:grid-cols-3 gap-6' variants={staggerContainer} initial='initial' whileInView='animate' viewport={{ once: true }}>
|
<motion.div
|
||||||
<TestimonialCard quote='跑了一晚上 NOFX,开源的 AI 自动交易,太有意思了,一晚上赚了 6% 收益!' author='@DIYgod' delay={0} />
|
className='grid md:grid-cols-3 gap-6'
|
||||||
<TestimonialCard quote='所有成功人士都在用 NOFX。IYKYK。' author='@SexyMichill' delay={0.1} />
|
variants={staggerContainer}
|
||||||
<TestimonialCard quote='NOFX 复兴了传奇 Alpha Arena,AI 驱动的加密期货战场。' author='@hqmank' delay={0.2} />
|
initial='initial'
|
||||||
|
whileInView='animate'
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
{items.map((item, idx) => (
|
||||||
|
<TestimonialCard key={idx} {...item} />
|
||||||
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedSection>
|
</AnimatedSection>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user