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:
ZhouYongyou
2025-11-03 18:40:54 +08:00
34 changed files with 4691 additions and 686 deletions

View File

@@ -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
View 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 前,检查你的标题是否:
- [ ] 包含有效的 typefeat, fix, docs 等)
- [ ] 使用小写字母开头
- [ ] 使用现在时态("add" 而不是 "added"
- [ ] 简洁明了(最好在 50 字符内)
- [ ] 准确描述了变更内容
**记住:** 这些都是建议,不是强制要求!

View File

@@ -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
View 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)

View File

@@ -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
View 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
View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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{

View File

@@ -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)

View File

@@ -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

View File

@@ -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 合并池

View 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

View 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/ETH20倍山寨币
---
#### ❌ 持仓无法执行
**检查以下内容:**
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

View File

@@ -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

View File

@@ -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.501 个交易员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

View File

@@ -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

View File

@@ -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,

View File

@@ -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
- 🌐 多链支持ETHBSCPolygon - 🌐 多链支持ETHBSCPolygon
- 🌍 无需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. 连接你的主钱包MetaMaskWalletConnect等 2. 访问[Aster API钱包](https://www.asterdex.com/en/api-wallet)
3. 点击"创建API钱包" 3. 连接你的主钱包MetaMaskWalletConnect等
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
View File

@@ -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
View File

@@ -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
View File

@@ -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
View 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
View 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)
}
}

View File

@@ -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("Midterm series (15minute 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 (20period): %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 (7Period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.RSI7Values)))
}
if len(data.MidTermSeries15m.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14Period): %s\n\n", formatFloatSlice(data.MidTermSeries15m.RSI14Values)))
}
}
if data.MidTermSeries1h != nil {
sb.WriteString("Midterm series (1hour 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 (20period): %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 (7Period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.RSI7Values)))
}
if len(data.MidTermSeries1h.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14Period): %s\n\n", formatFloatSlice(data.MidTermSeries1h.RSI14Values)))
}
}
if data.LongerTermContext != nil { if data.LongerTermContext != nil {
sb.WriteString("Longerterm context (4hour timeframe):\n\n") sb.WriteString("Longerterm context (4hour timeframe):\n\n")

260
market/monitor.go Normal file
View 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
View 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
View 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)
}
}

View 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各时间框架
- EMA504小时框架
- 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时强行交易无视策略失效信号。
- ❌ 情绪化决策和报复性交易,被短期波动左右。
- ❌ 过度自信忽视风险控制,放宽开仓或仓位标准。
---
**核心提示**:你拥有完整的技术分析自主权,基于提供的多维数据自由构建交易逻辑。特别注意:震荡行情完全由你自主分析处理,我们不过多干预你的分析判断。

View File

@@ -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>

View File

@@ -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 ArenaAI 驱动的加密期货战场。' 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>
) )
} }