diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..46a29814 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,135 @@ +# CODEOWNERS +# +# This file defines code ownership and automatic reviewer assignment. +# When a PR touches files matching these patterns, the listed users/teams +# will be automatically requested for review. +# +# 此文件定义代码所有权和自动 reviewer 分配。 +# 当 PR 涉及匹配这些模式的文件时,列出的用户/团队将自动被请求审查。 +# +# Syntax | 语法: +# pattern @username @org/team-name +# +# More specific patterns override less specific ones +# 更具体的模式会覆盖不太具体的模式 +# +# Documentation: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners + +# ============================================================================= +# Global Owners | 全局所有者 +# These users will be requested for review on ALL pull requests +# 这些用户将被请求审查所有 PR +# ============================================================================= + +* @hzb1115 @Icyoung @tangmengqiu @xqliu @SkywalkerJi + +# ============================================================================= +# Specific Component Owners | 特定组件所有者 +# Additional reviewers based on file paths (in addition to global owners) +# 基于文件路径的额外 reviewers(在全局 owners 之外) +# ============================================================================= + +# Backend / Go Code | 后端 / Go 代码 +# Go files and backend logic +*.go @xqliu @SkywalkerJi @hzb1115 @Icyoung @tangmengqiu +go.mod @xqliu @SkywalkerJi @hzb1115 @Icyoung @tangmengqiu +go.sum @xqliu @SkywalkerJi @hzb1115 @Icyoung @tangmengqiu + + +# Frontend / Web | 前端 / Web +# React/TypeScript frontend code +/web/ @0xEmberZz @hzb1115 @xqliu @tangmengqiu +/web/src/ @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.tsx @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.ts @0xEmberZz @hzb1115 @xqliu @tangmengqiu (frontend TypeScript only) +*.jsx @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.css @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.scss @0xEmberZz @hzb1115 @xqliu @tangmengqiu + +# Configuration Files | 配置文件 +*.json @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.yaml @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.yml @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.toml @0xEmberZz @hzb1115 @xqliu @tangmengqiu +*.ini @0xEmberZz @hzb1115 @xqliu @tangmengqiu + +# Documentation | 文档 +# Markdown and documentation files +*.md @hzb1115 @tangmengqiu +/docs/ @hzb1115 @tangmengqiu +README.md @hzb1115 @tangmengqiu + +# GitHub Workflows & Actions | GitHub 工作流和 Actions +# CI/CD configuration and automation +/.github/ @hzb1115 +/.github/workflows/ @hzb1115 +/.github/workflows/*.yml @hzb1115 + +# Docker | Docker 配置 +Dockerfile @tangmengqiu +docker-compose.yml @tangmengqiu +.dockerignore @tangmengqiu + +# Database | 数据库 +# Database migrations and schemas +/migrations/ @SkywalkerJi @hzb1115 @Icyoung @tangmengqiu +/db/ @SkywalkerJi @hzb1115 @Icyoung @tangmengqiu +*.sql @SkywalkerJi @hzb1115 @Icyoung @tangmengqiu + +# Scripts | 脚本 +/scripts/ @hzb1115 @xqliu @tangmengqiu +*.sh @hzb1115 @xqliu @tangmengqiu +*.bash @hzb1115 @tangmengqiu +*.py @hzb1115 @tangmengqiu (if Python scripts exist) + +# Tests | 测试 +# Test files require review from component owners +*_test.go @xqliu @SkywalkerJi @heronsbillC +/tests/ @xqliu @SkywalkerJi @Icyoung @heronsbillC +/web/tests/ @Icyoung @hzb1115 @heronsbillC + +# Security & Dependencies | 安全和依赖 +# Security-sensitive files require extra attention +.env.example @hzb1115 @tangmengqiu +.gitignore @hzb1115 @tangmengqiu +go.sum @xqliu @hzb1115 @tangmengqiu +package-lock.json @Icyoung @hzb1115 @tangmengqiu +yarn.lock @Icyoung @hzb1115 @tangmengqiu + +# Build Configuration | 构建配置 +Makefile @hzb1115 @xqliu @tangmengqiu +/build/ @hzb1115 @xqliu @tangmengqiu +/dist/ @hzb1115 @tangmengqiu + +# License & Legal | 许可证和法律文件 +LICENSE @hzb1115 +COPYING @hzb1115 + +# ============================================================================= +# Notes | 注意事项 +# ============================================================================= +# +# 1. All PRs will be assigned to the 5 global owners +# 所有 PR 都会分配给这 5 个全局 owners +# +# 2. Specific paths may add additional reviewers +# 特定路径可能会添加额外的 reviewers +# +# 3. PR author will NOT be requested for review (GitHub handles this) +# PR 作者不会被请求审查(GitHub 自动处理) +# +# 4. You can adjust patterns and owners as needed +# 你可以根据需要调整模式和 owners +# +# 5. To require multiple approvals, configure branch protection rules +# 要求多个批准,请配置分支保护规则 +# +# ⚠️ IMPORTANT - Permission Requirements | 重要 - 权限要求: +# - Users listed here will ONLY be auto-requested if they have Write+ permission +# 这里列出的用户只有在拥有 Write 或以上权限时才会被自动请求 +# - GitHub will silently skip users without proper permissions +# GitHub 会静默跳过没有适当权限的用户 +# - See CODEOWNERS_PERMISSIONS.md for details +# 详见 CODEOWNERS_PERMISSIONS.md +# +# ============================================================================= diff --git a/.github/ISSUE_TEMPLATE/bounty_claim.md b/.github/ISSUE_TEMPLATE/bounty_claim.md index b8fc97eb..1f76c159 100644 --- a/.github/ISSUE_TEMPLATE/bounty_claim.md +++ b/.github/ISSUE_TEMPLATE/bounty_claim.md @@ -82,7 +82,7 @@ By claiming this bounty, I acknowledge that: - [ ] I have read the [Contributing Guide](../../CONTRIBUTING.md) - [ ] I will follow the [Code of Conduct](../../CODE_OF_CONDUCT.md) - [ ] I understand the acceptance criteria -- [ ] My contribution will be licensed under MIT License +- [ ] My contribution will be licensed under AGPL-3.0 License - [ ] Payment is subject to successful PR merge --- diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 8d6a71b0..4ca3deb3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,33 +1,44 @@ # Pull Request | PR 提交 -> **💡 提示 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) +> **📋 选择专用模板 | Choose Specialized Template** +> +> 我们现在提供了针对不同类型PR的专用模板,帮助你更快速地填写PR信息: +> We now offer specialized templates for different types of PRs to help you fill out the information faster: +> +> - 🔧 **[Backend PR Template](./PULL_REQUEST_TEMPLATE/backend.md)** | 后端PR模板 - For Go/API/Trading changes +> - 🎨 **[Frontend PR Template](./PULL_REQUEST_TEMPLATE/frontend.md)** | 前端PR模板 - For UI/UX changes +> - 📝 **[Documentation PR Template](./PULL_REQUEST_TEMPLATE/docs.md)** | 文档PR模板 - For documentation updates +> - 📦 **[General PR Template](./PULL_REQUEST_TEMPLATE/general.md)** | 通用PR模板 - For mixed or other changes +> +> **如何使用?| How to use?** +> - 创建PR时,在URL中添加 `?template=backend.md` 或其他模板名称 +> - When creating a PR, add `?template=backend.md` or other template name to the URL +> - 或者直接复制粘贴对应模板的内容 +> - Or simply copy and paste the content from the corresponding template + +--- + +> **💡 提示 Tip:** 推荐 PR 标题格式 `type(scope): description` +> 例如: `feat(trader): add new strategy` | `fix(api): resolve auth issue` --- ## 📝 Description | 描述 - - +**English:** | **中文:** -**English:** -**中文:** --- ## 🎯 Type of Change | 变更类型 - - - -- [ ] 🐛 Bug fix | 修复 Bug(不影响现有功能的修复) -- [ ] ✨ New feature | 新功能(不影响现有功能的新增) -- [ ] 💥 Breaking change | 破坏性变更(会导致现有功能无法正常工作的修复或功能) +- [ ] 🐛 Bug fix | 修复 Bug +- [ ] ✨ New feature | 新功能 +- [ ] 💥 Breaking change | 破坏性变更 - [ ] 📝 Documentation update | 文档更新 -- [ ] 🎨 Code style update | 代码样式更新(格式化、重命名等) -- [ ] ♻️ Refactoring | 重构(无功能变更) +- [ ] 🎨 Code style update | 代码样式更新 +- [ ] ♻️ Refactoring | 重构 - [ ] ⚡ Performance improvement | 性能优化 - [ ] ✅ Test update | 测试更新 - [ ] 🔧 Build/config change | 构建/配置变更 @@ -37,9 +48,6 @@ ## 🔗 Related Issues | 相关 Issue - - - - Closes # | 关闭 # - Related to # | 相关 # @@ -47,242 +55,50 @@ ## 📋 Changes Made | 具体变更 - - - -**English:** -- Change 1 -- Change 2 -- Change 3 - -**中文:** -- 变更 1 -- 变更 2 -- 变更 3 +**English:** | **中文:** +- +- --- ## 🧪 Testing | 测试 -### Manual Testing | 手动测试 - - - - - [ ] Tested locally | 本地测试通过 -- [ ] Tested on testnet | 测试网测试通过(交易所集成相关) -- [ ] Tested with different configurations | 测试了不同配置 +- [ ] Tests pass | 测试通过 - [ ] Verified no existing functionality broke | 确认没有破坏现有功能 -### Test Environment | 测试环境 - -- **OS | 操作系统:** [e.g. macOS, Ubuntu, Windows] -- **Go Version | Go 版本:** [e.g. 1.21.5] -- **Node Version | Node 版本:** [e.g. 18.x] (if applicable | 如适用) -- **Exchange | 交易所:** [if applicable | 如适用] - -### Test Results | 测试结果 - - - - -``` -Test output here | 测试输出 -``` - ---- - -## 📸 Screenshots / Demo | 截图/演示 - - - - - - - -**Before | 变更前:** - - -**After | 变更后:** - - --- ## ✅ Checklist | 检查清单 - - - ### Code Quality | 代码质量 - -- [ ] My code follows the project's code style | 我的代码遵循项目代码风格 ([Contributing Guide](../CONTRIBUTING.md)) -- [ ] I have performed a self-review of my code | 我已进行代码自查 -- [ ] I have commented my code, particularly in hard-to-understand areas | 我已添加代码注释,特别是难以理解的部分 -- [ ] My changes generate no new warnings or errors | 我的变更没有产生新的警告或错误 -- [ ] Code compiles successfully | 代码编译成功 (`go build` / `npm run build`) -- [ ] I have run `go fmt` (for Go code) | 我已运行 `go fmt`(Go 代码) -- [ ] I have run `npm run lint` (for frontend code) | 我已运行 `npm run lint`(前端代码) - -### Testing | 测试 - -- [ ] I have added tests that prove my fix is effective or that my feature works | 我已添加证明修复有效或功能正常的测试 -- [ ] New and existing unit tests pass locally | 新旧单元测试在本地通过 -- [ ] I have tested on testnet (for trading/exchange changes) | 我已在测试网测试(交易/交易所变更) -- [ ] Integration tests pass | 集成测试通过 +- [ ] Code follows project style | 代码遵循项目风格 +- [ ] Self-review completed | 已完成代码自查 +- [ ] Comments added for complex logic | 已添加必要注释 ### Documentation | 文档 - -- [ ] I have updated the documentation accordingly | 我已相应更新文档 -- [ ] I have updated the README if needed | 我已更新 README(如需要) -- [ ] I have added inline code comments where necessary | 我已在必要处添加代码注释 -- [ ] I have updated type definitions (for TypeScript changes) | 我已更新类型定义(TypeScript 变更) -- [ ] I have updated API documentation (if applicable) | 我已更新 API 文档(如适用) +- [ ] Updated relevant documentation | 已更新相关文档 ### Git - -- [ ] My commits follow the conventional commits format | 我的提交遵循 Conventional Commits 格式 (`feat:`, `fix:`, etc.) -- [ ] I have rebased my branch on the latest `dev` branch | 我已将分支 rebase 到最新的 `dev` 分支 -- [ ] There are no merge conflicts | 没有合并冲突 -- [ ] Commit messages are clear and descriptive | 提交信息清晰明确 - ---- - -## 🔒 Security Considerations | 安全考虑 - - - - -- [ ] No API keys or secrets are hardcoded | 没有硬编码 API 密钥或密钥 -- [ ] User inputs are properly validated | 用户输入已正确验证 -- [ ] No SQL injection vulnerabilities introduced | 未引入 SQL 注入漏洞 -- [ ] No XSS vulnerabilities introduced | 未引入 XSS 漏洞 -- [ ] Authentication/authorization properly handled | 认证/授权已正确处理 -- [ ] Sensitive data is encrypted | 敏感数据已加密 -- [ ] N/A (not security-related) | 不适用(非安全相关) - ---- - -## ⚡ Performance Impact | 性能影响 - - - - -- [ ] No significant performance impact | 无显著性能影响 -- [ ] Performance improved | 性能提升 -- [ ] Performance may be impacted (explain below) | 性能可能受影响(请在下方说明) - - - - -**English:** - -**中文:** - ---- - -## 🌐 Internationalization | 国际化 - - - - -- [ ] All user-facing text supports i18n | 所有面向用户的文本支持国际化 -- [ ] Both English and Chinese versions provided | 提供了中英文版本 -- [ ] N/A | 不适用 +- [ ] Commits follow conventional format | 提交遵循 Conventional Commits 格式 +- [ ] Rebased on latest `dev` branch | 已 rebase 到最新 `dev` 分支 +- [ ] No merge conflicts | 无合并冲突 --- ## 📚 Additional Notes | 补充说明 - - +**English:** | **中文:** -**English:** - -**中文:** --- -## 💰 For Bounty Claims | 赏金申请 +**By submitting this PR, I confirm | 提交此 PR,我确认:** - - - -- [ ] This PR is for bounty issue # | 此 PR 用于赏金 issue # -- [ ] All acceptance criteria from the bounty issue are met | 满足赏金 issue 的所有验收标准 -- [ ] I have included a demo video/screenshots | 我已包含演示视频/截图 -- [ ] I am ready for payment upon merge | 我准备好在合并后接收付款 - -**Payment Details | 付款详情:** +- [ ] I have read the [Contributing Guidelines](../CONTRIBUTING.md) | 已阅读贡献指南 +- [ ] I agree to the [Code of Conduct](../CODE_OF_CONDUCT.md) | 同意行为准则 +- [ ] My contribution is licensed under AGPL-3.0 | 贡献遵循 AGPL-3.0 许可证 --- -## 🙏 Reviewer Notes | 审查者注意事项 - - - - -**English:** - -**中文:** - ---- - -## 📋 PR Size Estimate | PR 大小估计 - - - - -- [ ] 🟢 Small (< 100 lines) | 小(< 100 行) -- [ ] 🟡 Medium (100-500 lines) | 中(100-500 行) -- [ ] 🔴 Large (> 500 lines) | 大(> 500 行) - - - - - - - ---- - -## 🎯 Review Focus Areas | 审查重点 - - - - -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:** -**提交此 PR,我确认:** - -- [ ] 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! | 感谢你的贡献!** diff --git a/.github/PULL_REQUEST_TEMPLATE/README.md b/.github/PULL_REQUEST_TEMPLATE/README.md new file mode 100644 index 00000000..f0478ba7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/README.md @@ -0,0 +1,213 @@ +# PR Templates | PR 模板 + +## 📋 模板概述 | Template Overview + +我们提供了4种针对不同类型PR的专用模板,帮助贡献者快速填写PR信息: +We offer 4 specialized templates for different types of PRs to help contributors quickly fill out PR information: + +### 1. 🔧 Backend Template | 后端模板 +**文件:** `backend.md` + +**适用于 | Use for:** +- Go代码变更 | Go code changes +- API端点开发 | API endpoint development +- 交易逻辑实现 | Trading logic implementation +- 后端性能优化 | Backend performance optimization +- 数据库相关改动 | Database-related changes + +**包含 | Includes:** +- Go测试环境配置 | Go test environment +- 安全考虑检查 | Security considerations +- 性能影响评估 | Performance impact assessment +- `go fmt` 和 `go build` 检查 | `go fmt` and `go build` checks + +### 2. 🎨 Frontend Template | 前端模板 +**文件:** `frontend.md` + +**适用于 | Use for:** +- UI/UX变更 | UI/UX changes +- React/Vue组件开发 | React/Vue component development +- 前端样式更新 | Frontend styling updates +- 浏览器兼容性修复 | Browser compatibility fixes +- 前端性能优化 | Frontend performance optimization + +**包含 | Includes:** +- 截图/演示要求 | Screenshots/demo requirements +- 浏览器测试清单 | Browser testing checklist +- 国际化检查 | Internationalization checks +- 响应式设计验证 | Responsive design verification +- `npm run lint` 和 `npm run build` 检查 | Linting and build checks + +### 3. 📝 Documentation Template | 文档模板 +**文件:** `docs.md` + +**适用于 | Use for:** +- README更新 | README updates +- API文档编写 | API documentation +- 教程和指南 | Tutorials and guides +- 代码注释改进 | Code comment improvements +- 翻译工作 | Translation work + +**包含 | Includes:** +- 文档类型分类 | Documentation type classification +- 内容质量检查 | Content quality checks +- 双语要求(中英文)| Bilingual requirements (EN/CN) +- 链接有效性验证 | Link validity verification + +### 4. 📦 General Template | 通用模板 +**文件:** `general.md` + +**适用于 | Use for:** +- 混合类型变更 | Mixed-type changes +- 跨多个领域的PR | Cross-domain PRs +- 构建配置变更 | Build configuration changes +- 依赖更新 | Dependency updates +- 不确定使用哪个模板时 | When unsure which template to use + +## 🤖 自动模板建议 | Automatic Template Suggestion + +我们的GitHub Action会自动分析你的PR并建议最合适的模板: +Our GitHub Action automatically analyzes your PR and suggests the most suitable template: + +### 工作原理 | How it works: + +1. **文件分析 | File Analysis** + - 检测PR中所有变更的文件类型 + - Detects all changed file types in the PR + +2. **智能判断 | Smart Detection** + - 如果 >50% 是 `.go` 文件 → 建议**后端模板** + - If >50% are `.go` files → Suggests **Backend template** + - 如果 >50% 是 `.js/.ts/.tsx/.vue` 文件 → 建议**前端模板** + - If >50% are `.js/.ts/.tsx/.vue` files → Suggests **Frontend template** + - 如果 >70% 是 `.md` 文件 → 建议**文档模板** + - If >70% are `.md` files → Suggests **Documentation template** + +3. **自动评论 | Auto-comment** + - 如果检测到你使用了默认模板,但应该用专用模板 + - If it detects you're using the default template but should use a specialized one + - 会自动添加友好的评论建议 + - It will automatically add a friendly comment suggestion + +4. **自动标签 | Auto-labeling** + - 自动添加对应的标签:`backend`、`frontend`、`documentation` + - Automatically adds corresponding labels: `backend`, `frontend`, `documentation` + +## 📖 使用方法 | How to Use + +### 方法1: URL参数(推荐) | Method 1: URL Parameter (Recommended) + +创建PR时,在URL末尾添加模板参数: +When creating a PR, add the template parameter to the URL: + +``` +https://github.com/YOUR_ORG/nofx/compare/dev...YOUR_BRANCH?template=backend.md +``` + +替换 `backend.md` 为: +Replace `backend.md` with: +- `backend.md` - 后端模板 | Backend template +- `frontend.md` - 前端模板 | Frontend template +- `docs.md` - 文档模板 | Documentation template +- `general.md` - 通用模板 | General template + +### 方法2: 手动选择 | Method 2: Manual Selection + +1. 创建PR时,默认模板会显示 + When creating a PR, the default template will be shown + +2. 根据顶部的指引链接,点击查看对应的模板 + Follow the guidance links at the top to view the corresponding template + +3. 复制模板内容到PR描述中 + Copy the template content into the PR description + +### 方法3: 跟随自动建议 | Method 3: Follow Auto-suggestion + +1. 使用任何模板创建PR + Create a PR with any template + +2. GitHub Action会自动分析并评论建议 + GitHub Action will automatically analyze and comment with a suggestion + +3. 根据建议更新PR描述 + Update the PR description based on the suggestion + +## 🎯 最佳实践 | Best Practices + +1. **提前选择 | Choose in Advance** + - 在创建PR前确定变更类型 + - Determine the change type before creating the PR + +2. **完整填写 | Complete Filling** + - 不要跳过必填项(标记为 required) + - Don't skip required items + +3. **保持简洁 | Keep it Concise** + - 描述清晰但简洁 + - Keep descriptions clear but concise + +4. **添加截图 | Add Screenshots** + - 对于UI变更,务必添加截图 + - For UI changes, always add screenshots + +5. **测试证明 | Test Evidence** + - 提供测试通过的证据 + - Provide evidence that tests pass + +## 🔧 自定义 | Customization + +如果需要修改模板或自动检测逻辑: +If you need to modify templates or auto-detection logic: + +1. **修改模板** | **Modify Templates** + - 编辑 `.github/PULL_REQUEST_TEMPLATE/*.md` 文件 + - Edit `.github/PULL_REQUEST_TEMPLATE/*.md` files + +2. **调整检测阈值** | **Adjust Detection Threshold** + - 编辑 `.github/workflows/pr-template-suggester.yml` + - Edit `.github/workflows/pr-template-suggester.yml` + - 修改文件类型占比阈值(当前:50%后端,50%前端,70%文档) + - Modify file type percentage thresholds (current: 50% backend, 50% frontend, 70% docs) + +3. **添加新模板** | **Add New Template** + - 在 `PULL_REQUEST_TEMPLATE/` 目录创建新的 `.md` 文件 + - Create a new `.md` file in the `PULL_REQUEST_TEMPLATE/` directory + - 更新工作流以支持新的文件类型检测 + - Update the workflow to support new file type detection + +## ❓ FAQ + +**Q: 我的PR既有前端又有后端代码,用哪个模板?** +**Q: My PR has both frontend and backend code, which template should I use?** + +A: 使用**通用模板**(`general.md`),或选择主要变更类型的模板。 +A: Use the **General template** (`general.md`), or choose the template for the primary change type. + +--- + +**Q: 自动建议的模板不合适怎么办?** +**Q: What if the automatically suggested template is not suitable?** + +A: 你可以忽略建议,继续使用当前模板。自动建议仅供参考。 +A: You can ignore the suggestion and continue using the current template. Auto-suggestions are for reference only. + +--- + +**Q: 可以不使用任何模板吗?** +**Q: Can I not use any template?** + +A: 不推荐。模板帮助确保PR包含必要信息,加快审查速度。 +A: Not recommended. Templates help ensure PRs contain necessary information and speed up reviews. + +--- + +**Q: 如何禁用自动模板建议?** +**Q: How to disable automatic template suggestions?** + +A: 删除或禁用 `.github/workflows/pr-template-suggester.yml` 文件。 +A: Delete or disable the `.github/workflows/pr-template-suggester.yml` file. + +--- + +🌟 **感谢使用我们的PR模板系统!| Thank you for using our PR template system!** diff --git a/.github/PULL_REQUEST_TEMPLATE/backend.md b/.github/PULL_REQUEST_TEMPLATE/backend.md new file mode 100644 index 00000000..dfc354c3 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/backend.md @@ -0,0 +1,121 @@ +# Pull Request - Backend | 后端 PR + +> **💡 提示 Tip:** 推荐 PR 标题格式 `type(scope): description` +> 例如: `feat(trader): add new strategy` | `fix(api): resolve auth issue` + +--- + +## 📝 Description | 描述 + +**English:** | **中文:** + + + +--- + +## 🎯 Type of Change | 变更类型 + +- [ ] 🐛 Bug fix | 修复 Bug +- [ ] ✨ New feature | 新功能 +- [ ] 💥 Breaking change | 破坏性变更 +- [ ] ♻️ Refactoring | 重构 +- [ ] ⚡ Performance improvement | 性能优化 +- [ ] 🔒 Security fix | 安全修复 +- [ ] 🔧 Build/config change | 构建/配置变更 + +--- + +## 🔗 Related Issues | 相关 Issue + +- Closes # | 关闭 # +- Related to # | 相关 # + +--- + +## 📋 Changes Made | 具体变更 + +**English:** | **中文:** +- +- + +--- + +## 🧪 Testing | 测试 + +### Test Environment | 测试环境 +- **OS | 操作系统:** +- **Go Version | Go 版本:** +- **Exchange | 交易所:** [if applicable | 如适用] + +### Manual Testing | 手动测试 +- [ ] Tested locally | 本地测试通过 +- [ ] Tested on testnet | 测试网测试通过(交易所集成相关) +- [ ] Unit tests pass | 单元测试通过 +- [ ] Verified no existing functionality broke | 确认没有破坏现有功能 + +### Test Results | 测试结果 +``` +Test output here | 测试输出 +``` + +--- + +## 🔒 Security Considerations | 安全考虑 + +- [ ] No API keys or secrets hardcoded | 没有硬编码 API 密钥 +- [ ] User inputs properly validated | 用户输入已正确验证 +- [ ] No SQL injection vulnerabilities | 无 SQL 注入漏洞 +- [ ] Authentication/authorization properly handled | 认证/授权正确处理 +- [ ] Sensitive data is encrypted | 敏感数据已加密 +- [ ] N/A (not security-related) | 不适用 + +--- + +## ⚡ Performance Impact | 性能影响 + +- [ ] No significant performance impact | 无显著性能影响 +- [ ] Performance improved | 性能提升 +- [ ] Performance may be impacted (explain below) | 性能可能受影响 + +**If impacted, explain | 如果受影响,请说明:** + + +--- + +## ✅ Checklist | 检查清单 + +### Code Quality | 代码质量 +- [ ] Code follows project style | 代码遵循项目风格 +- [ ] Self-review completed | 已完成代码自查 +- [ ] Comments added for complex logic | 已添加必要注释 +- [ ] Code compiles successfully | 代码编译成功 (`go build`) +- [ ] Ran `go fmt` | 已运行 `go fmt` + +### Documentation | 文档 +- [ ] Updated relevant documentation | 已更新相关文档 +- [ ] Added inline comments where necessary | 已添加必要的代码注释 +- [ ] Updated API documentation (if applicable) | 已更新 API 文档 + +### Git +- [ ] Commits follow conventional format | 提交遵循 Conventional Commits 格式 +- [ ] Rebased on latest `dev` branch | 已 rebase 到最新 `dev` 分支 +- [ ] No merge conflicts | 无合并冲突 + +--- + +## 📚 Additional Notes | 补充说明 + +**English:** | **中文:** + + +--- + +**By submitting this PR, I confirm | 提交此 PR,我确认:** + +- [ ] I have read the [Contributing Guidelines](../../CONTRIBUTING.md) | 已阅读贡献指南 +- [ ] I agree to the [Code of Conduct](../../CODE_OF_CONDUCT.md) | 同意行为准则 +- [ ] My contribution is licensed under AGPL-3.0 | 贡献遵循 AGPL-3.0 许可证 + +--- + +🌟 **Thank you for your contribution! | 感谢你的贡献!** diff --git a/.github/PULL_REQUEST_TEMPLATE/docs.md b/.github/PULL_REQUEST_TEMPLATE/docs.md new file mode 100644 index 00000000..2ce9a90c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/docs.md @@ -0,0 +1,97 @@ +# Pull Request - Documentation | 文档 PR + +> **💡 提示 Tip:** 推荐 PR 标题格式 `docs(scope): description` +> 例如: `docs(api): update trading endpoints` | `docs(readme): add setup guide` + +--- + +## 📝 Description | 描述 + +**English:** | **中文:** + + +--- + +## 📚 Type of Documentation | 文档类型 + +- [ ] 📖 README update | README 更新 +- [ ] 📋 API documentation | API 文档 +- [ ] 🎓 Tutorial/Guide | 教程/指南 +- [ ] 📝 Code comments | 代码注释 +- [ ] 🔧 Configuration docs | 配置文档 +- [ ] 🐛 Fix typo/error | 修复拼写/错误 +- [ ] 🌍 Translation | 翻译 + +--- + +## 🔗 Related Issues | 相关 Issue + +- Closes # | 关闭 # +- Related to # | 相关 # + +--- + +## 📋 Changes Made | 具体变更 + +**English:** | **中文:** +- +- + +--- + +## 📸 Screenshots (if applicable) | 截图(如适用) + + + + + +--- + +## 🌐 Internationalization | 国际化 + +- [ ] English version complete | 英文版本完整 +- [ ] Chinese version complete | 中文版本完整 +- [ ] Both versions are consistent | 两个版本内容一致 +- [ ] N/A (only one language needed) | 不适用(只需要一种语言) + +--- + +## ✅ Checklist | 检查清单 + +### Content Quality | 内容质量 +- [ ] Information is accurate and up-to-date | 信息准确且最新 +- [ ] Language is clear and concise | 语言清晰简洁 +- [ ] No spelling or grammar errors | 无拼写或语法错误 +- [ ] Links are valid and working | 链接有效且可用 +- [ ] Code examples are tested and working | 代码示例已测试且可用 +- [ ] Formatting is consistent | 格式一致 + +### Documentation Standards | 文档标准 +- [ ] Follows project documentation style | 遵循项目文档风格 +- [ ] Includes necessary examples | 包含必要的示例 +- [ ] Technical terms are explained | 技术术语已解释 +- [ ] Self-review completed | 已完成自查 + +### Git +- [ ] Commits follow conventional format | 提交遵循 Conventional Commits 格式 +- [ ] Rebased on latest `dev` branch | 已 rebase 到最新 `dev` 分支 +- [ ] No merge conflicts | 无合并冲突 + +--- + +## 📚 Additional Notes | 补充说明 + +**English:** | **中文:** + + +--- + +**By submitting this PR, I confirm | 提交此 PR,我确认:** + +- [ ] I have read the [Contributing Guidelines](../../CONTRIBUTING.md) | 已阅读贡献指南 +- [ ] I agree to the [Code of Conduct](../../CODE_OF_CONDUCT.md) | 同意行为准则 +- [ ] My contribution is licensed under AGPL-3.0 | 贡献遵循 AGPL-3.0 许可证 + +--- + +🌟 **Thank you for your contribution! | 感谢你的贡献!** diff --git a/.github/PULL_REQUEST_TEMPLATE/frontend.md b/.github/PULL_REQUEST_TEMPLATE/frontend.md new file mode 100644 index 00000000..b95a20d0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/frontend.md @@ -0,0 +1,119 @@ +# Pull Request - Frontend | 前端 PR + +> **💡 提示 Tip:** 推荐 PR 标题格式 `type(scope): description` +> 例如: `feat(ui): add dark mode toggle` | `fix(form): resolve validation bug` + +--- + +## 📝 Description | 描述 + +**English:** | **中文:** + + +--- + +## 🎯 Type of Change | 变更类型 + +- [ ] 🐛 Bug fix | 修复 Bug +- [ ] ✨ New feature | 新功能 +- [ ] 💥 Breaking change | 破坏性变更 +- [ ] 🎨 Code style update | 代码样式更新 +- [ ] ♻️ Refactoring | 重构 +- [ ] ⚡ Performance improvement | 性能优化 + +--- + +## 🔗 Related Issues | 相关 Issue + +- Closes # | 关闭 # +- Related to # | 相关 # + +--- + +## 📋 Changes Made | 具体变更 + +**English:** | **中文:** +- +- + +--- + +## 📸 Screenshots / Demo | 截图/演示 + + + + +**Before | 变更前:** + + +**After | 变更后:** + + +--- + +## 🧪 Testing | 测试 + +### Test Environment | 测试环境 +- **OS | 操作系统:** +- **Node Version | Node 版本:** +- **Browser(s) | 浏览器:** + +### Manual Testing | 手动测试 +- [ ] Tested in development mode | 开发模式测试通过 +- [ ] Tested production build | 生产构建测试通过 +- [ ] Tested on multiple browsers | 多浏览器测试通过 +- [ ] Tested responsive design | 响应式设计测试通过 +- [ ] Verified no existing functionality broke | 确认没有破坏现有功能 + +--- + +## 🌐 Internationalization | 国际化 + +- [ ] All user-facing text supports i18n | 所有面向用户的文本支持国际化 +- [ ] Both English and Chinese versions provided | 提供了中英文版本 +- [ ] N/A | 不适用 + +--- + +## ✅ Checklist | 检查清单 + +### Code Quality | 代码质量 +- [ ] Code follows project style | 代码遵循项目风格 +- [ ] Self-review completed | 已完成代码自查 +- [ ] Comments added for complex logic | 已添加必要注释 +- [ ] Code builds successfully | 代码构建成功 (`npm run build`) +- [ ] Ran `npm run lint` | 已运行 `npm run lint` +- [ ] No console errors or warnings | 无控制台错误或警告 + +### Testing | 测试 +- [ ] Component tests added/updated | 已添加/更新组件测试 +- [ ] Tests pass locally | 测试在本地通过 + +### Documentation | 文档 +- [ ] Updated relevant documentation | 已更新相关文档 +- [ ] Updated type definitions (TypeScript) | 已更新类型定义 +- [ ] Added JSDoc comments where necessary | 已添加 JSDoc 注释 + +### Git +- [ ] Commits follow conventional format | 提交遵循 Conventional Commits 格式 +- [ ] Rebased on latest `dev` branch | 已 rebase 到最新 `dev` 分支 +- [ ] No merge conflicts | 无合并冲突 + +--- + +## 📚 Additional Notes | 补充说明 + +**English:** | **中文:** + + +--- + +**By submitting this PR, I confirm | 提交此 PR,我确认:** + +- [ ] I have read the [Contributing Guidelines](../../CONTRIBUTING.md) | 已阅读贡献指南 +- [ ] I agree to the [Code of Conduct](../../CODE_OF_CONDUCT.md) | 同意行为准则 +- [ ] My contribution is licensed under AGPL-3.0 | 贡献遵循 AGPL-3.0 许可证 + +--- + +🌟 **Thank you for your contribution! | 感谢你的贡献!** diff --git a/.github/PULL_REQUEST_TEMPLATE/general.md b/.github/PULL_REQUEST_TEMPLATE/general.md new file mode 100644 index 00000000..23773e4c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/general.md @@ -0,0 +1,98 @@ +# Pull Request - General | 通用 PR + +> **💡 提示 Tip:** 推荐 PR 标题格式 `type(scope): description` +> 例如: `feat(trader): add new strategy` | `fix(api): resolve auth issue` | `docs(readme): update` + +--- + +## 📝 Description | 描述 + +**English:** | **中文:** + + +--- + +## 🎯 Type of Change | 变更类型 + +- [ ] 🐛 Bug fix | 修复 Bug +- [ ] ✨ New feature | 新功能 +- [ ] 💥 Breaking change | 破坏性变更 +- [ ] 📝 Documentation update | 文档更新 +- [ ] 🎨 Code style update | 代码样式更新 +- [ ] ♻️ Refactoring | 重构 +- [ ] ⚡ Performance improvement | 性能优化 +- [ ] ✅ Test update | 测试更新 +- [ ] 🔧 Build/config change | 构建/配置变更 +- [ ] 🔒 Security fix | 安全修复 + +--- + +## 🔗 Related Issues | 相关 Issue + +- Closes # | 关闭 # +- Related to # | 相关 # + +--- + +## 📋 Changes Made | 具体变更 + +**English:** | **中文:** +- +- + +--- + +## 🧪 Testing | 测试 + +- [ ] Tested locally | 本地测试通过 +- [ ] Tests pass | 测试通过 +- [ ] Verified no existing functionality broke | 确认没有破坏现有功能 + +**Test details | 测试详情:** + + +--- + +## ✅ Checklist | 检查清单 + +### Code Quality | 代码质量 +- [ ] Code follows project style | 代码遵循项目风格 +- [ ] Self-review completed | 已完成代码自查 +- [ ] Comments added for complex logic | 已添加必要注释 +- [ ] No new warnings or errors | 无新的警告或错误 + +### Documentation | 文档 +- [ ] Updated relevant documentation | 已更新相关文档 +- [ ] Added inline comments where necessary | 已添加必要的代码注释 + +### Git +- [ ] Commits follow conventional format | 提交遵循 Conventional Commits 格式 +- [ ] Rebased on latest `dev` branch | 已 rebase 到最新 `dev` 分支 +- [ ] No merge conflicts | 无合并冲突 + +--- + +## 🔒 Security (if applicable) | 安全(如适用) + +- [ ] No API keys or secrets hardcoded | 没有硬编码 API 密钥 +- [ ] User inputs properly validated | 用户输入已正确验证 +- [ ] N/A | 不适用 + +--- + +## 📚 Additional Notes | 补充说明 + +**English:** | **中文:** + + +--- + +**By submitting this PR, I confirm | 提交此 PR,我确认:** + +- [ ] I have read the [Contributing Guidelines](../../CONTRIBUTING.md) | 已阅读贡献指南 +- [ ] I agree to the [Code of Conduct](../../CODE_OF_CONDUCT.md) | 同意行为准则 +- [ ] My contribution is licensed under AGPL-3.0 | 贡献遵循 AGPL-3.0 许可证 + +--- + +🌟 **Thank you for your contribution! | 感谢你的贡献!** diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..84036674 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,95 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - main + - dev + tags: + - 'v*' + pull_request: + branches: + - main + - dev + workflow_dispatch: + +env: + REGISTRY_GHCR: ghcr.io + IMAGE_NAME_BACKEND: ${{ github.repository }}/nofx-backend + IMAGE_NAME_FRONTEND: ${{ github.repository }}/nofx-frontend + +jobs: + build-and-push: + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + + strategy: + matrix: + include: + - name: backend + dockerfile: ./docker/Dockerfile.backend + image_suffix: backend + - name: frontend + dockerfile: ./docker/Dockerfile.frontend + image_suffix: frontend + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY_GHCR }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + continue-on-error: true + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.REGISTRY_GHCR }}/${{ github.repository }}/nofx-${{ matrix.image_suffix }} + ${{ secrets.DOCKERHUB_USERNAME && format('{0}/nofx-{1}', secrets.DOCKERHUB_USERNAME, matrix.image_suffix) || '' }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha,prefix={{branch}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push ${{ matrix.name }} image + uses: docker/build-push-action@v5 + with: + context: . + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + BUILD_DATE=${{ github.event.head_commit.timestamp }} + VCS_REF=${{ github.sha }} + VERSION=${{ github.ref_name }} + + - name: Image digest + run: echo "Image digest for ${{ matrix.name }} - ${{ steps.meta.outputs.digest }}" diff --git a/.github/workflows/pr-checks-comment.yml b/.github/workflows/pr-checks-comment.yml index db0983f8..8e46508f 100644 --- a/.github/workflows/pr-checks-comment.yml +++ b/.github/workflows/pr-checks-comment.yml @@ -104,6 +104,53 @@ jobs: echo "⚠️ Frontend results artifact not found" fi + - name: Get PR information + id: pr-info + if: steps.backend.outputs.pr_number != '0' + uses: actions/github-script@v7 + with: + script: | + const prNumber = ${{ steps.backend.outputs.pr_number }}; + + // Get PR details + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + // Check PR title format (Conventional Commits) + const prTitle = pr.title; + const conventionalCommitPattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|security|build)(\(.+\))?: .+/; + const titleValid = conventionalCommitPattern.test(prTitle); + + core.setOutput('pr_title', prTitle); + core.setOutput('title_valid', titleValid); + + // Calculate PR size + const additions = pr.additions; + const deletions = pr.deletions; + const total = additions + deletions; + + let size = ''; + let sizeEmoji = ''; + if (total < 300) { + size = 'Small'; + sizeEmoji = '🟢'; + } else if (total < 1000) { + size = 'Medium'; + sizeEmoji = '🟡'; + } else { + size = 'Large'; + sizeEmoji = '🔴'; + } + + core.setOutput('pr_size', size); + core.setOutput('size_emoji', sizeEmoji); + core.setOutput('total_lines', total); + core.setOutput('additions', additions); + core.setOutput('deletions', deletions); + - name: Post advisory results comment if: steps.backend.outputs.pr_number != '0' uses: actions/github-script@v7 @@ -113,7 +160,40 @@ jobs: 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'; + + // PR Information section + const prTitle = '${{ steps.pr-info.outputs.pr_title }}'; + const titleValid = '${{ steps.pr-info.outputs.title_valid }}' === 'true'; + const prSize = '${{ steps.pr-info.outputs.pr_size }}'; + const sizeEmoji = '${{ steps.pr-info.outputs.size_emoji }}'; + const totalLines = '${{ steps.pr-info.outputs.total_lines }}'; + const additions = '${{ steps.pr-info.outputs.additions }}'; + const deletions = '${{ steps.pr-info.outputs.deletions }}'; + + comment += '### 📋 PR Information\n\n'; + + // Title check + if (titleValid) { + comment += '**Title Format:** ✅ Good - Follows Conventional Commits\n'; + } else { + comment += '**Title Format:** ⚠️ Suggestion - Consider using `type(scope): description`\n'; + comment += '
Recommended format\n\n'; + comment += '**Valid types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `security`, `build`\n\n'; + comment += '**Examples:**\n'; + comment += '- `feat(trader): add new trading strategy`\n'; + comment += '- `fix(api): resolve authentication issue`\n'; + comment += '- `docs: update README`\n'; + comment += '
\n\n'; + } + + // Size check + comment += `**PR Size:** ${sizeEmoji} ${prSize} (${totalLines} lines: +${additions} -${deletions})\n`; + + if (prSize === 'Large') { + comment += '\n💡 **Suggestion:** This is a large PR. Consider breaking it into smaller, focused PRs for easier review.\n'; + } + + comment += '\n'; // Backend checks const fmtStatus = '${{ steps.backend.outputs.fmt_status }}'; @@ -208,37 +288,71 @@ jobs: return; } - const prNumber = pulls.data[0].number; + const pr = pulls.data[0]; + const prNumber = pr.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'); + // Get PR information for fallback comment + const prTitle = pr.title; + const conventionalCommitPattern = /^(feat|fix|docs|style|refactor|perf|test|chore|ci|security|build)(\(.+\))?: .+/; + const titleValid = conventionalCommitPattern.test(prTitle); + + const additions = pr.additions || 0; + const deletions = pr.deletions || 0; + const total = additions + deletions; + + let size = ''; + let sizeEmoji = ''; + if (total < 300) { + size = 'Small'; + sizeEmoji = '🟢'; + } else if (total < 1000) { + size = 'Medium'; + sizeEmoji = '🟡'; + } else { + size = 'Large'; + sizeEmoji = '🔴'; + } + + let comment = '## ⚠️ Advisory Checks - Results Unavailable\n\n'; + comment += 'The advisory checks workflow completed, but results could not be retrieved.\n\n'; + + // Add PR Information + comment += '### 📋 PR Information\n\n'; + + if (titleValid) { + comment += '**Title Format:** ✅ Good - Follows Conventional Commits\n'; + } else { + comment += '**Title Format:** ⚠️ Suggestion - Consider using `type(scope): description`\n'; + } + + comment += `**PR Size:** ${sizeEmoji} ${size} (${total} lines: +${additions} -${deletions})\n\n`; + + if (size === 'Large') { + comment += '💡 **Suggestion:** This is a large PR. Consider breaking it into smaller, focused PRs for easier review.\n\n'; + } + + comment += '---\n\n'; + comment += '### ⚠️ Backend/Frontend Check Results\n\n'; + comment += 'Results could not be retrieved.\n\n'; + comment += '**Possible reasons:**\n'; + comment += '- Artifacts were not uploaded successfully\n'; + comment += '- Artifacts expired (retention: 1 day)\n'; + comment += '- Permission issues\n\n'; + comment += '**What to do:**\n'; + comment += `1. Check the [PR Checks - Run workflow](${context.payload.workflow_run?.html_url || 'logs'}) logs\n`; + comment += '2. Ensure your code passes local checks:\n'; + comment += '```bash\n'; + comment += '# Backend\n'; + comment += 'go fmt ./...\n'; + comment += 'go vet ./...\n'; + comment += 'go build\n'; + comment += 'go test ./...\n\n'; + comment += '# Frontend (if applicable)\n'; + comment += 'cd web\n'; + comment += 'npm run build\n'; + comment += '```\n\n'; + comment += '---\n\n'; + comment += '*This is an automated fallback message. The advisory checks ran but results are not available.*'; await github.rest.issues.createComment({ issue_number: prNumber, diff --git a/.github/workflows/pr-template-suggester.yml b/.github/workflows/pr-template-suggester.yml new file mode 100644 index 00000000..9e74a9e4 --- /dev/null +++ b/.github/workflows/pr-template-suggester.yml @@ -0,0 +1,189 @@ +name: PR Template Suggester + +on: + pull_request: + types: [opened, edited, synchronize] + +permissions: + pull-requests: write + contents: read + +jobs: + suggest-template: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Analyze PR files and auto-apply template + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + let goFiles = 0, jsFiles = 0, tsFiles = 0, mdFiles = 0, otherFiles = 0; + + for (const file of files) { + const filename = file.filename.toLowerCase(); + if (filename.endsWith('.go')) goFiles++; + else if (filename.endsWith('.js') || filename.endsWith('.jsx')) jsFiles++; + else if (filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.vue')) tsFiles++; + else if (filename.endsWith('.md')) mdFiles++; + else otherFiles++; + } + + const totalFiles = goFiles + jsFiles + tsFiles + mdFiles + otherFiles; + if (totalFiles === 0) { console.log('No files changed'); return; } + + let suggestedTemplate = null, templateEmoji = '', templateLabel = ''; + + if (goFiles / totalFiles > 0.5) { + suggestedTemplate = 'backend'; templateEmoji = '🔧'; templateLabel = 'backend'; + } else if ((jsFiles + tsFiles) / totalFiles > 0.5) { + suggestedTemplate = 'frontend'; templateEmoji = '🎨'; templateLabel = 'frontend'; + } else if (mdFiles / totalFiles > 0.7) { + suggestedTemplate = 'docs'; templateEmoji = '📝'; templateLabel = 'documentation'; + } + + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + }); + + const prBody = pr.body || ''; + const usesBackendTemplate = prBody.includes('Pull Request - Backend'); + const usesFrontendTemplate = prBody.includes('Pull Request - Frontend'); + const usesDocsTemplate = prBody.includes('Pull Request - Documentation'); + const usesGeneralTemplate = prBody.includes('Pull Request - General'); + const usingDefaultTemplate = !usesBackendTemplate && !usesFrontendTemplate && !usesDocsTemplate && !usesGeneralTemplate; + + if (templateLabel) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [templateLabel] + }); + console.log('Added label: ' + templateLabel); + } catch (error) { + console.log('Label might not exist, skipping...'); + } + } + + function isPRBodyEmpty(body) { + if (!body || body.trim().length < 100) return true; + const hasEmptyDescription = body.includes('**English:**') && body.match(/\*\*English:\*\*\s*\n\s*\n\s*\n/); + const hasEmptyChanges = body.includes('具体变更') && body.match(/\*\*中文:\*\*\s*\n\s*-\s*\n\s*-\s*\n/); + if (hasEmptyDescription || hasEmptyChanges) return true; + const descMatch = body.match(/\*\*English:\*\*[||]\s*\*\*中文:\*\*\s*\n\s*(.+)/); + if (!descMatch || descMatch[1].trim().length < 10) return true; + return false; + } + + if (suggestedTemplate && usingDefaultTemplate) { + const shouldAutoApply = isPRBodyEmpty(prBody); + const templatePath = '.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md'; + + if (shouldAutoApply) { + try { + const { data: templateFile } = await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: templatePath, + ref: context.payload.pull_request.head.ref + }); + + const templateContent = Buffer.from(templateFile.content, 'base64').toString('utf-8'); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + body: templateContent + }); + + console.log('Auto-applied ' + suggestedTemplate + ' template'); + + let fileStats = []; + if (goFiles > 0) fileStats.push('- 🔧 Go files: ' + goFiles); + if (jsFiles > 0) fileStats.push('- 🎨 JavaScript files: ' + jsFiles); + if (tsFiles > 0) fileStats.push('- 🎨 TypeScript files: ' + tsFiles); + if (mdFiles > 0) fileStats.push('- 📝 Markdown files: ' + mdFiles); + if (otherFiles > 0) fileStats.push('- 📦 Other files: ' + otherFiles); + const fileStatsText = fileStats.join('\n'); + + const notifyComment = '## ' + templateEmoji + ' 已自动应用专用模板 | Auto-Applied Template\n\n' + + '检测到您的PR主要包含 **' + suggestedTemplate + '** 相关的变更,系统已自动为您应用相应的模板。\n\n' + + 'Detected that your PR primarily contains **' + suggestedTemplate + '** changes. The appropriate template has been automatically applied.\n\n' + + '**文件统计 | File Statistics**\n' + fileStatsText + '\n\n' + + '**已应用模板 | Applied Template**\n`' + templatePath + '`\n\n' + + '✨ 您现在可以直接在PR描述中填写相关信息了!\n\n' + + '✨ You can now fill in the relevant information in the PR description!'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: notifyComment + }); + + } catch (error) { + console.log('Failed to fetch or apply template: ' + error.message); + const templateUrl = 'https://raw.githubusercontent.com/' + context.repo.owner + '/' + context.repo.repo + '/dev/.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md'; + const fallbackComment = '## ' + templateEmoji + ' 建议使用专用模板 | Suggested Template\n\n' + + '您的PR主要包含 **' + suggestedTemplate + '** 相关的变更。\n\n' + + '**推荐模板 | Recommended Template:** `.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md`\n\n' + + '**如何使用 | How to use:** [点击查看模板内容](' + templateUrl + ')'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: fallbackComment + }); + } + } else { + console.log('PR body has content, sending suggestion only'); + + let fileStats = []; + if (goFiles > 0) fileStats.push('- 🔧 Go files: ' + goFiles); + if (jsFiles > 0) fileStats.push('- 🎨 JavaScript files: ' + jsFiles); + if (tsFiles > 0) fileStats.push('- 🎨 TypeScript files: ' + tsFiles); + if (mdFiles > 0) fileStats.push('- 📝 Markdown files: ' + mdFiles); + if (otherFiles > 0) fileStats.push('- 📦 Other files: ' + otherFiles); + const fileStatsText = fileStats.join('\n'); + + const templateUrl = 'https://raw.githubusercontent.com/' + context.repo.owner + '/' + context.repo.repo + '/dev/.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md'; + + const comment = '## ' + templateEmoji + ' 建议使用专用模板 | Suggested Template\n\n' + + '您的PR主要包含 **' + suggestedTemplate + '** 相关的变更。我们建议使用更适合的模板以简化填写。\n\n' + + 'Your PR primarily contains **' + suggestedTemplate + '** changes. We suggest using a more suitable template to simplify filling.\n\n' + + '**文件统计 | File Statistics**\n' + fileStatsText + '\n\n' + + '**推荐模板 | Recommended Template**\n```\n.github/PULL_REQUEST_TEMPLATE/' + suggestedTemplate + '.md\n```\n\n' + + '**如何使用 | How to use**\n' + + '1. 编辑PR描述 | Edit PR description\n' + + '2. 复制 [' + suggestedTemplate + ' 模板内容](' + templateUrl + ') | Copy [' + suggestedTemplate + ' template content](' + templateUrl + ')\n' + + '3. 或在创建PR时使用URL参数 | Or use URL parameter when creating PR\n' + + ' `?template=' + suggestedTemplate + '.md`\n\n' + + '_这是一个自动建议,您可以继续使用当前模板。_\n\n' + + '_This is an automated suggestion. You may continue using the current template._'; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } + } else if (suggestedTemplate && !usingDefaultTemplate) { + console.log('PR already uses a specific template'); + } else { + console.log('No specific template suggestion needed - mixed changes'); + } diff --git a/.gitignore b/.gitignore index ad0d2a5b..d595c953 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ web/node_modules/ node_modules/ web/dist/ web/.vite/ + +# ESLint 临时报告文件(调试时生成,不纳入版本控制) +eslint-*.json diff --git a/.husky/_/husky.sh b/.husky/_/husky.sh new file mode 100755 index 00000000..cec959a6 --- /dev/null +++ b/.husky/_/husky.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh +if [ -z "$husky_skip_init" ]; then + debug () { + if [ "$HUSKY_DEBUG" = "1" ]; then + echo "husky (debug) - $1" + fi + } + + readonly hook_name="$(basename -- "$0")" + debug "starting $hook_name..." + + if [ "$HUSKY" = "0" ]; then + debug "HUSKY env variable is set to 0, skipping hook" + exit 0 + fi + + if [ -f ~/.huskyrc ]; then + debug "sourcing ~/.huskyrc" + . ~/.huskyrc + fi + + readonly husky_skip_init=1 + export husky_skip_init + sh -e "$0" "$@" + exitCode="$?" + + if [ $exitCode != 0 ]; then + echo "husky - $hook_name hook exited with code $exitCode (error)" + fi + + if [ $exitCode = 127 ]; then + echo "husky - command not found in PATH=$PATH" + fi + + exit $exitCode +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000..25b3e6b7 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +cd web && npx lint-staged diff --git a/DOCKER_DEPLOY.ja.md b/DOCKER_DEPLOY.ja.md new file mode 100644 index 00000000..e4a1fe9e --- /dev/null +++ b/DOCKER_DEPLOY.ja.md @@ -0,0 +1,472 @@ +# 🐳 Dockerワンクリックデプロイガイド + +このガイドは、Dockerを使用してNOFX AIトレーディング競争システムを迅速にデプロイする方法を説明します。 + +## 📋 前提条件 + +開始する前に、システムに以下が必要です: + +- **Docker**: バージョン20.10以上 +- **Docker Compose**: バージョン2.0以上 + +### Dockerのインストール + +#### macOS / Windows +[Docker Desktop](https://www.docker.com/products/docker-desktop/)をダウンロードしてインストール + +#### Linux (Ubuntu/Debian) + +> #### Docker Composeバージョンに関する注意 +> +> **新規ユーザー推奨:** +> - **Docker Desktopを使用**: 最新のDocker Composeが自動的に含まれ、別途インストールは不要 +> - シンプルなインストール、ワンクリックセットアップ、GUI管理を提供 +> - macOS、Windows、一部のLinuxディストリビューションをサポート +> +> **既存ユーザー向け注意:** +> - **スタンドアロンdocker-composeの非推奨**: 独立したDocker Composeバイナリのダウンロードは推奨されません +> - **組み込みバージョンを使用**: Docker 20.10+には`docker compose`コマンド(スペース付き)が含まれています +> - 古い`docker-compose`をまだ使用している場合は、新しい構文にアップグレードしてください + +*推奨:Docker Desktop(利用可能な場合)またはCompose組み込みのDocker CEを使用* + +```bash +# Dockerをインストール(composeを含む) +curl -fsSL https://get.docker.com -o get-docker.sh +sudo sh get-docker.sh + +# dockerグループにユーザーを追加 +sudo usermod -aG docker $USER +newgrp docker + +# インストールを確認(新しいコマンド) +docker --version +docker compose --version # Docker 24+にはこれが含まれており、別途インストール不要 +``` + +## 🚀 クイックスタート(3ステップ) + +### ステップ1:設定ファイルを準備 + +```bash +# 設定テンプレートをコピー +cp config.json.example config.json + +# APIキーで設定ファイルを編集 +nano config.json # または他のエディタを使用 +``` + +**必須フィールド:** +```json +{ + "traders": [ + { + "id": "my_trader", + "name": "My AI Trader", + "ai_model": "deepseek", + "binance_api_key": "YOUR_BINANCE_API_KEY", // ← BinanceのAPIキー + "binance_secret_key": "YOUR_BINANCE_SECRET_KEY", // ← Binanceのシークレットキー + "deepseek_key": "YOUR_DEEPSEEK_API_KEY", // ← DeepSeekのAPIキー + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080 +} +``` + +### ステップ2:ワンクリック起動 + +```bash +# すべてのサービスをビルドして起動(初回実行) +docker compose up -d --build + +# 以降の起動(リビルドなし) +docker compose up -d +``` + +**起動オプション:** +- `--build`: Dockerイメージをビルド(初回実行またはコード更新後に使用) +- `-d`: デタッチモードで実行(バックグラウンド) + +### ステップ3:システムにアクセス + +デプロイが完了したら、ブラウザを開いて以下にアクセス: + +- **Webインターフェース**: http://localhost:3000 +- **APIヘルスチェック**: http://localhost:8080/health + +## 📊 サービス管理 + +### 実行状態を表示 + +```bash +# すべてのコンテナステータスを表示 +docker compose ps + +# サービスヘルスステータスを表示 +docker compose ps --format json | jq +``` + +### ログを表示 + +```bash +# すべてのサービスログを表示 +docker compose logs -f + +# バックエンドログのみを表示 +docker compose logs -f backend + +# フロントエンドログのみを表示 +docker compose logs -f frontend + +# 最後の100行を表示 +docker compose logs --tail=100 +``` + +### サービスを停止 + +```bash +# すべてのサービスを停止(データを保持) +docker compose stop + +# コンテナを停止して削除(データを保持) +docker compose down + +# コンテナとボリュームを停止して削除(すべてのデータをクリア) +docker compose down -v +``` + +### サービスを再起動 + +```bash +# すべてのサービスを再起動 +docker compose restart + +# バックエンドのみを再起動 +docker compose restart backend + +# フロントエンドのみを再起動 +docker compose restart frontend +``` + +### サービスを更新 + +```bash +# 最新のコードをプル +git pull + +# リビルドして再起動 +docker compose up -d --build +``` + +## 🔧 高度な設定 + +### ポートを変更 + +`docker-compose.yml`を編集してポートマッピングを変更: + +```yaml +services: + backend: + ports: + - "8080:8080" # "your_port:8080"に変更 + + frontend: + ports: + - "3000:80" # "your_port:80"に変更 +``` + +### リソース制限 + +`docker-compose.yml`にリソース制限を追加: + +```yaml +services: + backend: + deploy: + resources: + limits: + cpus: '2' + memory: 2G + reservations: + cpus: '1' + memory: 1G +``` + +### 環境変数 + +`.env`ファイルを作成して環境変数を管理: + +```bash +# .env +TZ=Asia/Tokyo +BACKEND_PORT=8080 +FRONTEND_PORT=3000 +``` + +次に`docker-compose.yml`で使用: + +```yaml +services: + backend: + ports: + - "${BACKEND_PORT}:8080" +``` + +## 📁 データの永続化 + +システムは自動的にデータをローカルディレクトリに永続化します: + +- `./decision_logs/`: AI判断ログ +- `./coin_pool_cache/`: コインプールキャッシュ +- `./config.json`: 設定ファイル(マウント済み) + +**データの場所:** +```bash +# データディレクトリを表示 +ls -la decision_logs/ +ls -la coin_pool_cache/ + +# データをバックアップ +tar -czf backup_$(date +%Y%m%d).tar.gz decision_logs/ coin_pool_cache/ config.json + +# データを復元 +tar -xzf backup_20241029.tar.gz +``` + +## 🐛 トラブルシューティング + +### コンテナが起動しない + +```bash +# 詳細なエラーメッセージを表示 +docker compose logs backend +docker compose logs frontend + +# コンテナステータスを確認 +docker compose ps -a + +# リビルド(キャッシュをクリア) +docker compose build --no-cache +``` + +### ポートが既に使用中 + +```bash +# ポートを使用しているプロセスを検索 +lsof -i :8080 # バックエンドポート +lsof -i :3000 # フロントエンドポート + +# プロセスを強制終了 +kill -9 +``` + +### 設定ファイルが見つからない + +```bash +# config.jsonが存在することを確認 +ls -la config.json + +# 存在しない場合、テンプレートをコピー +cp config.json.example config.json +``` + +### ヘルスチェックが失敗 + +```bash +# ヘルスステータスを確認 +docker inspect nofx-backend | jq '.[0].State.Health' +docker inspect nofx-frontend | jq '.[0].State.Health' + +# ヘルスエンドポイントを手動でテスト +curl http://localhost:8080/health +curl http://localhost:3000/health +``` + +### フロントエンドがバックエンドに接続できない + +```bash +# ネットワーク接続を確認 +docker compose exec frontend ping backend + +# バックエンドサービスが実行中か確認 +docker compose exec frontend wget -O- http://backend:8080/health +``` + +### Dockerリソースをクリーン + +```bash +# 未使用のイメージをクリーン +docker image prune -a + +# 未使用のボリュームをクリーン +docker volume prune + +# すべての未使用リソースをクリーン(注意して使用) +docker system prune -a --volumes +``` + +## 🔐 セキュリティ推奨事項 + +1. **config.jsonをGitにコミットしない** + ```bash + # config.jsonが.gitignoreに含まれていることを確認 + echo "config.json" >> .gitignore + ``` + +2. **機密データには環境変数を使用** + ```yaml + # docker-compose.yml + services: + backend: + environment: + - BINANCE_API_KEY=${BINANCE_API_KEY} + - BINANCE_SECRET_KEY=${BINANCE_SECRET_KEY} + ``` + +3. **APIアクセスを制限** + ```yaml + # ローカルアクセスのみを許可 + services: + backend: + ports: + - "127.0.0.1:8080:8080" + ``` + +4. **イメージを定期的に更新** + ```bash + docker compose pull + docker compose up -d + ``` + +## 🌐 本番環境デプロイ + +### Nginxリバースプロキシの使用 + +```nginx +# /etc/nginx/sites-available/nofx +server { + listen 80; + server_name your-domain.com; + + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + location /api/ { + proxy_pass http://localhost:8080/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### HTTPSの設定(Let's Encrypt) + +```bash +# Certbotをインストール +sudo apt-get install certbot python3-certbot-nginx + +# SSL証明書を取得 +sudo certbot --nginx -d your-domain.com + +# 自動更新 +sudo certbot renew --dry-run +``` + +### Docker Swarmの使用(クラスタデプロイ) + +```bash +# Swarmを初期化 +docker swarm init + +# スタックをデプロイ +docker stack deploy -c docker-compose.yml nofx + +# サービスステータスを表示 +docker stack services nofx + +# サービスをスケール +docker service scale nofx_backend=3 +``` + +## 📈 監視&ロギング + +### ログ管理 + +```bash +# ログローテーションを設定(docker-compose.ymlで既に設定済み) +logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +# ログ統計を表示 +docker compose logs --timestamps | wc -l +``` + +### 監視ツール統合 + +Prometheus + Grafanaで監視を統合: + +```yaml +# docker-compose.yml(監視サービスを追加) +services: + prometheus: + image: prom/prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana + ports: + - "3001:3000" +``` + +## 🆘 ヘルプを取得 + +- **GitHub Issues**: [Issueを提出](https://github.com/yourusername/open-nofx/issues) +- **ドキュメント**: [README.md](README.md)を確認 +- **コミュニティ**: Discord/Telegramグループに参加 + +## 📝 コマンドチートシート + +```bash +# 起動 +docker compose up -d --build # ビルドして起動 +docker compose up -d # 起動(リビルドなし) + +# 停止 +docker compose stop # サービスを停止 +docker compose down # コンテナを停止して削除 +docker compose down -v # コンテナとデータを停止して削除 + +# 表示 +docker compose ps # ステータスを表示 +docker compose logs -f # ログを表示 +docker compose top # プロセスを表示 + +# 再起動 +docker compose restart # すべてのサービスを再起動 +docker compose restart backend # バックエンドを再起動 + +# 更新 +git pull && docker compose up -d --build + +# クリーン +docker compose down -v # すべてのデータをクリア +docker system prune -a # Dockerリソースをクリーン +``` + +--- + +🎉 おめでとうございます!NOFX AIトレーディング競争システムのデプロイに成功しました! + +問題が発生した場合は、[トラブルシューティング](#-トラブルシューティング)セクションを確認するか、Issueを提出してください。 diff --git a/README.ja.md b/README.ja.md new file mode 100644 index 00000000..de215593 --- /dev/null +++ b/README.ja.md @@ -0,0 +1,1343 @@ +# 🤖 NOFX - Agentic Trading OS + +[![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) +[![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Backed by Amber.ac](https://img.shields.io/badge/Backed%20by-Amber.ac-orange.svg)](https://amber.ac) + +**言語:** [English](README.md) | [中文](README.zh-CN.md) | [Українська](README.uk.md) | [Русский](README.ru.md) | [日本語](README.ja.md) + +**公式Twitter:** [@nofx_ai](https://x.com/nofx_ai) + +--- + +## 🚀 ユニバーサルAIトレーディングOS + +**NOFX**は、統合アーキテクチャに基づいて構築された**ユニバーサルAgenticトレーディングOS**です。暗号通貨市場において **「マルチエージェント判断 → 統一リスク管理 → 低レイテンシ実行 → ライブ/ペーパーアカウントバックテスト」** のループを成功裏に完成させ、現在この技術スタックを **株式、先物、オプション、外国為替、およびすべての金融市場** に拡大しています。 + +### 🎯 コア機能 + +- **ユニバーサルデータ&バックテストレイヤー**: クロスマーケット、クロスタイムフレーム、クロス取引所の統一表現とファクターライブラリにより、転移可能な「戦略メモリ」を蓄積 +- **マルチエージェント自己対戦&自己進化**: 戦略が自動的に競争し、最適なものを選択、アカウントレベルのPnLとリスク制約に基づいて継続的に反復 +- **統合実行&リスク管理**: 低レイテンシルーティング、スリッページ/リスク管理サンドボックス、アカウントレベルの制限、ワンクリック市場切り替え + +### 🏢 [Amber.ac](https://amber.ac)の支援 + +### 👥 コアチーム + +- **Tinkle** - [@Web3Tinkle](https://x.com/Web3Tinkle) +- **Zack** - [@0x_ZackH](https://x.com/0x_ZackH) + +### 💼 シードラウンド募集中 + +現在、**シードラウンド**の資金調達を行っています。 + +**投資に関するお問い合わせ**は、TwitterでTinkleまたはZackにDMをお送りください。 + +**パートナーシップおよび協業**については、公式Twitter [@nofx_ai](https://x.com/nofx_ai)にDMをお送りください。 + +--- + +> ⚠️ **リスク警告**: このシステムは実験的なものです。AI自動取引には大きなリスクが伴います。学習/研究目的、または少額でのテストのみを強く推奨します! + +## 👥 開発者コミュニティ + +Telegram開発者コミュニティに参加して、議論、アイデアの共有、サポートを受けましょう: + +**💬 [NOFX開発者コミュニティ](https://t.me/nofx_dev_community)** + +--- + +## 🆕 最新情報(最新アップデート) + +### 🚀 マルチ取引所対応! + +NOFXは現在、**3つの主要取引所**をサポートしています:Binance、Hyperliquid、Aster DEX! + +#### **Hyperliquid取引所** + +高性能な分散型無期限先物取引所! + +**主な機能:** +- ✅ フル取引サポート(ロング/ショート、レバレッジ、ストップロス/テイクプロフィット) +- ✅ 自動精度処理(注文サイズ&価格) +- ✅ 統一トレーダーインターフェース(シームレスな取引所切り替え) +- ✅ メインネットとテストネットの両方をサポート +- ✅ APIキー不要 - Ethereum秘密鍵のみ + +**なぜHyperliquid?** +- 🔥 中央集権型取引所より低い手数料 +- 🔒 非カストディアル - 資金を自分で管理 +- ⚡ オンチェーン決済による高速実行 +- 🌍 KYC不要 + +**クイックスタート:** +1. MetaMaskの秘密鍵を取得(`0x`プレフィックスを削除) +2. config.jsonで`"exchange": "hyperliquid"`を設定 +3. `"hyperliquid_private_key": "your_key"`を追加 +4. 取引開始! + +詳細は[設定ガイド](#-代替hyperliquid取引所の使用)をご覧ください。 + +#### **Aster DEX取引所**(NEW! v2.0.2) + +Binance互換の分散型無期限先物取引所! + +**主な機能:** +- ✅ BinanceスタイルAPI(Binanceからの移行が簡単) +- ✅ Web3ウォレット認証(安全で分散型) +- ✅ 自動精度処理によるフル取引サポート +- ✅ CEXより低い取引手数料 +- ✅ EVM互換(Ethereum、BSC、Polygonなど) + +**なぜAster?** +- 🎯 **Binance互換API** - 最小限のコード変更で済む +- 🔐 **APIウォレットシステム** - セキュリティのための独立した取引ウォレット +- 💰 **競争力のある手数料** - ほとんどの中央集権型取引所より低い +- 🌐 **マルチチェーンサポート** - お好みのEVMチェーンで取引 + +**クイックスタート:** +1. [Aster APIウォレット](https://www.asterdex.com/en/api-wallet)にアクセス +2. メインウォレットを接続してAPIウォレットを作成 +3. API Signerアドレスと秘密鍵をコピー +4. config.jsonで`"exchange": "aster"`を設定 +5. `"aster_user"`、`"aster_signer"`、`"aster_private_key"`を追加 + +--- + +## 📸 スクリーンショット + +### 🏆 競争モード - リアルタイムAIバトル +![競争ページ](screenshots/competition-page.png) +*QwenとDeepSeekのライブトレーディングバトルを示すリアルタイムパフォーマンス比較チャート付きマルチAIリーダーボード* + +### 📊 トレーダー詳細 - 完全なトレーディングダッシュボード +![詳細ページ](screenshots/details-page.png) +*エクイティカーブ、ライブポジション、展開可能な入力プロンプトと思考連鎖推論を持つAI判断ログを備えたプロフェッショナルな取引インターフェース* + +--- + +## ✨ 現在の実装 - 暗号通貨市場 + +NOFXは現在、以下の実証済み機能で**暗号通貨市場において完全に稼働**しています: + +### 🏆 マルチエージェント競争フレームワーク +- **ライブエージェントバトル**: QwenとDeepSeekモデルがリアルタイム取引で競争 +- **独立したアカウント管理**: 各エージェントは独自の判断ログとパフォーマンスメトリクスを維持 +- **リアルタイムパフォーマンス比較**: ライブROI追跡、勝率統計、一対一分析 +- **自己進化ループ**: エージェントは過去のパフォーマンスから学習し、継続的に改善 + +### 🧠 AI自己学習&最適化 +- **過去フィードバックシステム**: 各判断前に過去20取引サイクルを分析 +- **スマートパフォーマンス分析**: + - 最高/最悪パフォーマンス資産の特定 + - 実際のUSDT建てで勝率、損益比、平均利益を計算 + - 繰り返しミスを回避(連続損失パターン) + - 成功戦略を強化(高勝率パターン) +- **動的戦略調整**: AIはバックテスト結果に基づいて取引スタイルを自律的に適応 + +### 📊 ユニバーサルマーケットデータレイヤー(暗号実装) +- **マルチタイムフレーム分析**: 3分リアルタイム + 4時間トレンドデータ +- **テクニカル指標**: EMA20/50、MACD、RSI(7/14)、ATR +- **建玉追跡**: マーケットセンチメント、資金フロー分析 +- **流動性フィルタリング**: 低流動性資産(<1500万USD)の自動フィルタリング +- **クロス取引所サポート**: 統一データインターフェースでBinance、Hyperliquid、Aster DEX + +### 🎯 統一リスク管理システム +- **ポジション制限**: 資産ごとの制限(アルトコイン≤1.5x エクイティ、BTC/ETH≤10x エクイティ) +- **設定可能なレバレッジ**: 資産クラスとアカウントタイプに基づいて1xから50xまでの動的レバレッジ +- **証拠金管理**: 総使用量≤90%、AI制御配分 +- **リスクリワード強制**: 必須≥1:2 ストップロス対テイクプロフィット比率 +- **重複防止**: 同じ資産/方向での重複ポジションを防止 + +### ⚡ 低レイテンシ実行エンジン +- **マルチ取引所API統合**: Binance Futures、Hyperliquid DEX、Aster DEX +- **自動精度処理**: 取引所ごとのスマートな注文サイズと価格フォーマット +- **優先実行**: 既存ポジションを先にクローズし、その後新規を開く +- **スリッページ管理**: 実行前検証、リアルタイム精度チェック + +### 🎨 プロフェッショナルモニタリングインターフェース +- **Binanceスタイルダッシュボード**: リアルタイム更新付きプロフェッショナルダークテーマ +- **エクイティカーブ**: 過去のアカウント価値追跡(USD/パーセンテージ切り替え) +- **パフォーマンスチャート**: ライブ更新付きマルチエージェントROI比較 +- **完全な判断ログ**: すべての取引の完全な思考連鎖(CoT)推論 +- **5秒データ更新**: リアルタイムアカウント、ポジション、損益更新 + +--- + +## 🔮 ロードマップ - ユニバーサルマーケット拡大 + +実証済みの暗号インフラストラクチャを以下に拡張中: + +- **📈 株式市場**: 米国株式、A株、香港株 +- **📊 先物市場**: 商品先物、指数先物 +- **🎯 オプション取引**: 株式オプション、暗号オプション +- **💱 外国為替市場**: 主要通貨ペア、クロスレート + +**同じアーキテクチャ。同じエージェントフレームワーク。すべての市場。** + +--- + +## 🏗️ 技術アーキテクチャ + +``` +nofx/ +├── main.go # プログラムエントリ(マルチトレーダーマネージャー) +├── config.json # 設定ファイル(APIキー、マルチトレーダー設定) +│ +├── api/ # HTTP APIサービス +│ └── server.go # Ginフレームワーク、RESTful API +│ +├── trader/ # トレーディングコア +│ ├── auto_trader.go # 自動取引メインコントローラー(単一トレーダー) +│ └── binance_futures.go # Binance先物APIラッパー +│ +├── manager/ # マルチトレーダー管理 +│ └── trader_manager.go # 複数のトレーダーインスタンスを管理 +│ +├── mcp/ # Model Context Protocol - AI通信 +│ └── client.go # AIクライアント(DeepSeek/Qwen統合) +│ +├── decision/ # AI判断エンジン +│ └── engine.go # 過去フィードバック付き判断ロジック +│ +├── market/ # マーケットデータ取得 +│ └── data.go # マーケットデータ&テクニカル指標(K線、RSI、MACD) +│ +├── pool/ # コインプール管理 +│ └── coin_pool.go # AI500 + OI Topマージプール +│ +├── logger/ # ロギングシステム +│ └── decision_logger.go # 判断記録 + パフォーマンス分析 +│ +├── decision_logs/ # 判断ログストレージ +│ ├── qwen_trader/ # Qwenトレーダーログ +│ └── deepseek_trader/ # DeepSeekトレーダーログ +│ +└── web/ # Reactフロントエンド + ├── src/ + │ ├── components/ # Reactコンポーネント + │ │ ├── EquityChart.tsx # エクイティカーブチャート + │ │ ├── ComparisonChart.tsx # マルチAI比較チャート + │ │ └── CompetitionPage.tsx # 競争リーダーボード + │ ├── lib/api.ts # API呼び出しラッパー + │ ├── types/index.ts # TypeScript型 + │ ├── index.css # BinanceスタイルCSS + │ └── App.tsx # メインアプリ + └── package.json +``` + +### コア依存関係 + +**バックエンド(Go)** +- `github.com/adshao/go-binance/v2` - Binance APIクライアント +- `github.com/markcheno/go-talib` - テクニカル指標計算(TA-Lib) +- `github.com/gin-gonic/gin` - HTTP APIフレームワーク + +**フロントエンド(React + TypeScript)** +- `react` + `react-dom` - UIフレームワーク +- `recharts` - チャートライブラリ(エクイティカーブ、比較チャート) +- `swr` - データフェッチングとキャッシング +- `tailwindcss` - CSSフレームワーク + +--- + +## 💰 Binanceアカウント登録(手数料節約!) + +このシステムを使用する前に、Binance先物アカウントが必要です。**紹介リンクを使用して取引手数料を節約しましょう:** + +**🎁 [Binance登録 - 手数料割引を取得](https://www.binance.com/join?ref=TINKLEVIP)** + +### 登録手順: + +1. **上記のリンクをクリック**してBinance登録ページにアクセス +2. メール/電話番号で**登録を完了** +3. **KYC認証を完了**(先物取引に必要) +4. **先物アカウントを有効化**: + - Binanceホームページ → デリバティブ → USDT無期限先物 + - 「今すぐ開設」をクリックして先物取引を有効化 +5. **APIキーを作成**: + - アカウント → API管理 + - 新しいAPIキーを作成、**「先物」権限を有効化** + - APIキーとシークレットキーを保存(config.jsonに必要) + - **重要**: セキュリティのためIPアドレスをホワイトリストに追加 + +### 手数料割引の利点: + +- ✅ **現物取引**: 最大30%の手数料割引 +- ✅ **先物取引**: 最大30%の手数料割引 +- ✅ **生涯有効**: すべての取引で永久割引 + +--- + +## 🚀 クイックスタート + +### 🐳 オプションA:Dockerワンクリックデプロイ(最も簡単 - 初心者推奨!) + +**⚡ Dockerで3つの簡単なステップで取引開始 - インストール不要!** + +Dockerはすべての依存関係(Go、Node.js、TA-Lib)と環境設定を自動的に処理します。初心者に最適! + +#### ステップ1:設定を準備 + +```bash +# 設定テンプレートをコピー +cp config.json.example config.json + +# 編集してAPIキーを入力 +nano config.json # または任意のエディタを使用 +``` + +#### ステップ2:ワンクリック起動 + +```bash +# オプション1:便利スクリプトを使用(推奨) +chmod +x start.sh +./start.sh start --build + +> #### Docker Composeバージョンに関する注意 +> +> **このプロジェクトはDocker Compose V2構文(スペース付き)を使用** +> +> 古いスタンドアロン`docker-compose`がインストールされている場合は、Docker DesktopまたはDocker 20.10+にアップグレードしてください + +# オプション2:docker composeを直接使用 +docker compose up -d --build +``` + +#### ステップ3:ダッシュボードにアクセス + +ブラウザを開いて次にアクセス:**http://localhost:3000** + +**これで完了!🎉** AIトレーディングシステムが稼働中です! + +#### システム管理 + +```bash +./start.sh logs # ログを表示 +./start.sh status # ステータスを確認 +./start.sh stop # サービスを停止 +./start.sh restart # サービスを再起動 +``` + +**📖 詳細なDockerデプロイガイド、トラブルシューティング、高度な設定について:** +- **English**: See [DOCKER_DEPLOY.en.md](DOCKER_DEPLOY.en.md) +- **中文**: 查看 [DOCKER_DEPLOY.md](DOCKER_DEPLOY.md) +- **日本語**: [DOCKER_DEPLOY.ja.md](DOCKER_DEPLOY.ja.md)を参照 + +--- + +### 📦 オプションB:手動インストール(開発者向け) + +**注意**: 上記のDockerデプロイを使用した場合は、このセクションをスキップしてください。手動インストールは、コードを変更したい場合、またはDockerなしで実行したい場合にのみ必要です。 + +### 1. 環境要件 + +- **Go 1.21+** +- **Node.js 18+** +- **TA-Lib**ライブラリ(テクニカル指標計算) + +#### TA-Libのインストール + +**macOS:** +```bash +brew install ta-lib +``` + +**Ubuntu/Debian:** +```bash +sudo apt-get install libta-lib0-dev +``` + +**その他のシステム**: [TA-Lib公式ドキュメント](https://github.com/markcheno/go-talib)を参照 + +### 2. プロジェクトをクローン + +```bash +git clone https://github.com/tinkle-community/nofx.git +cd nofx +``` + +### 3. 依存関係をインストール + +**バックエンド:** +```bash +go mod download +``` + +**フロントエンド:** +```bash +cd web +npm install +cd .. +``` + +### 4. AI APIキーを取得 + +システムを設定する前に、AI APIキーを取得する必要があります。以下のAIプロバイダーのいずれかを選択してください: + +#### オプション1:DeepSeek(初心者推奨) + +**なぜDeepSeek?** +- 💰 GPT-4より安価(約1/10のコスト) +- 🚀 高速レスポンス時間 +- 🎯 優れた取引判断品質 +- 🌍 VPNなしで世界中で動作 + +**DeepSeek APIキーの取得方法:** + +1. **アクセス**: [https://platform.deepseek.com](https://platform.deepseek.com) +2. **登録**: メール/電話番号でサインアップ +3. **認証**: メール/電話認証を完了 +4. **チャージ**: アカウントにクレジットを追加 + - 最低: 約$5 USD + - 推奨: テスト用に$20-50 USD +5. **APIキーを作成**: + - APIキーセクションに移動 + - 「新しいキーを作成」をクリック + - キーをコピーして保存(`sk-`で始まる) + - ⚠️ **重要**: すぐに保存してください - 再度見ることはできません! + +**価格**: 約100万トークンあたり$0.14(非常に安い!) + +#### オプション2:Qwen(Alibaba Cloud) + +**Qwen APIキーの取得方法:** + +1. **アクセス**: [https://dashscope.aliyuncs.com](https://dashscope.aliyuncs.com) +2. **登録**: Alibaba Cloudアカウントでサインアップ +3. **サービスを有効化**: DashScopeサービスを有効化 +4. **APIキーを作成**: + - APIキー管理に移動 + - 新しいキーを作成 + - コピーして保存(`sk-`で始まる) + +**注意**: 登録には中国の電話番号が必要な場合があります + +--- + +### 5. システム設定 + +**2つの設定モードが利用可能:** +- **🌟 初心者モード**: シングルトレーダー + デフォルトコイン(推奨!) +- **⚔️ エキスパートモード**: 複数トレーダー競争 + +#### 🌟 初心者モード設定(推奨) + +**ステップ1**: 設定例ファイルをコピーしてリネーム + +```bash +cp config.json.example config.json +``` + +**ステップ2**: APIキーで`config.json`を編集 + +```json +{ + "traders": [ + { + "id": "my_trader", + "name": "My AI Trader", + "ai_model": "deepseek", + "binance_api_key": "YOUR_BINANCE_API_KEY", + "binance_secret_key": "YOUR_BINANCE_SECRET_KEY", + "use_qwen": false, + "deepseek_key": "sk-xxxxxxxxxxxxx", + "qwen_key": "", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, + "use_default_coins": true, + "coin_pool_api_url": "", + "oi_top_api_url": "", + "api_server_port": 8080 +} +``` + +**ステップ3**: プレースホルダーを実際のキーに置き換え + +| プレースホルダー | 置き換え先 | 取得場所 | +|------------|--------------|--------------| +| `YOUR_BINANCE_API_KEY` | BinanceのAPIキー | Binance → アカウント → API管理 | +| `YOUR_BINANCE_SECRET_KEY` | Binanceのシークレットキー | 上記と同じ | +| `sk-xxxxxxxxxxxxx` | DeepSeek APIキー | [platform.deepseek.com](https://platform.deepseek.com) | + +**ステップ4**: 初期残高を調整(オプション) + +- `initial_balance`: 実際のBinance先物アカウント残高に設定 +- 損益パーセンテージの計算に使用 +- 例:500 USDTがある場合、`"initial_balance": 500.0`に設定 + +**✅ 設定チェックリスト:** + +- [ ] Binance APIキーを入力(引用符の問題なし) +- [ ] Binanceシークレットキーを入力(引用符の問題なし) +- [ ] DeepSeek APIキーを入力(`sk-`で始まる) +- [ ] `use_default_coins`を`true`に設定(初心者向け) +- [ ] `initial_balance`をアカウント残高と一致させる +- [ ] ファイルを`config.json`として保存(`.example`ではない) + +--- + +#### 🔷 代替:Hyperliquid取引所の使用 + +**NOFXはHyperliquidもサポート** - 分散型無期限先物取引所。Binanceの代わりにHyperliquidを使用するには: + +**ステップ1**: Ethereum秘密鍵を取得(Hyperliquid認証用) + +1. **MetaMask**(または任意のEthereumウォレット)を開く +2. 秘密鍵をエクスポート +3. キーから**`0x`プレフィックスを削除** +4. [Hyperliquid](https://hyperliquid.xyz)でウォレットに資金を入金 + +**ステップ2**: Hyperliquid用に`config.json`を設定 + +```json +{ + "traders": [ + { + "id": "hyperliquid_trader", + "name": "My Hyperliquid Trader", + "enabled": true, + "ai_model": "deepseek", + "exchange": "hyperliquid", + "hyperliquid_private_key": "your_private_key_without_0x", + "hyperliquid_wallet_addr": "your_ethereum_address", + "hyperliquid_testnet": false, + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080 +} +``` + +**Binance設定との主な違い:** +- `binance_api_key` + `binance_secret_key`を`hyperliquid_private_key`に置き換え +- `"exchange": "hyperliquid"`フィールドを追加 +- メインネットには`hyperliquid_testnet: false`、テストネットには`true`を設定 + +**⚠️ セキュリティ警告**: 秘密鍵は絶対に共有しないでください!メインウォレットではなく、取引専用のウォレットを使用してください。 + +--- + +#### 🔶 代替:Aster DEX取引所の使用 + +**NOFXはAster DEXもサポート** - Binance互換の分散型無期限先物取引所! + +**なぜAsterを選ぶ?** +- 🎯 Binance互換API(簡単な移行) +- 🔐 APIウォレットセキュリティシステム +- 💰 低い取引手数料 +- 🌐 マルチチェーンサポート(ETH、BSC、Polygon) +- 🌍 KYC不要 + +**ステップ1**: Aster APIウォレットを作成 + +1. [Aster APIウォレット](https://www.asterdex.com/en/api-wallet)にアクセス +2. メインウォレットを接続(MetaMask、WalletConnectなど) +3. 「APIウォレットを作成」をクリック +4. **これらの3つの項目をすぐに保存:** + - メインウォレットアドレス(User) + - APIウォレットアドレス(Signer) + - APIウォレット秘密鍵(⚠️ 一度だけ表示!) + +**ステップ2**: Aster用に`config.json`を設定 + +```json +{ + "traders": [ + { + "id": "aster_deepseek", + "name": "Aster DeepSeek Trader", + "enabled": true, + "ai_model": "deepseek", + "exchange": "aster", + + "aster_user": "0x63DD5aCC6b1aa0f563956C0e534DD30B6dcF7C4e", + "aster_signer": "0x21cF8Ae13Bb72632562c6Fff438652Ba1a151bb0", + "aster_private_key": "4fd0a42218f3eae43a6ce26d22544e986139a01e5b34a62db53757ffca81bae1", + + "deepseek_key": "sk-xxxxxxxxxxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + } +} +``` + +**主要設定フィールド:** +- `"exchange": "aster"` - 取引所をAsterに設定 +- `aster_user` - メインウォレットアドレス +- `aster_signer` - APIウォレットアドレス(ステップ1から) +- `aster_private_key` - APIウォレット秘密鍵(`0x`プレフィックスなし) + +**📖 詳細なセットアップ手順については**: [Aster統合ガイド](ASTER_INTEGRATION.md)を参照 + +**⚠️ セキュリティ注意事項**: +- APIウォレットはメインウォレットとは別(追加のセキュリティレイヤー) +- API秘密鍵は絶対に共有しない +- [asterdex.com](https://www.asterdex.com/en/api-wallet)でいつでもAPIウォレットアクセスを取り消し可能 + +--- + +#### ⚔️ エキスパートモード:マルチトレーダー競争 + +複数のAIトレーダーが互いに競争する場合: + +```json +{ + "traders": [ + { + "id": "qwen_trader", + "name": "Qwen AI Trader", + "ai_model": "qwen", + "binance_api_key": "YOUR_BINANCE_API_KEY_1", + "binance_secret_key": "YOUR_BINANCE_SECRET_KEY_1", + "use_qwen": true, + "qwen_key": "sk-xxxxx", + "deepseek_key": "", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + }, + { + "id": "deepseek_trader", + "name": "DeepSeek AI Trader", + "ai_model": "deepseek", + "binance_api_key": "YOUR_BINANCE_API_KEY_2", + "binance_secret_key": "YOUR_BINANCE_SECRET_KEY_2", + "use_qwen": false, + "qwen_key": "", + "deepseek_key": "sk-xxxxx", + "initial_balance": 1000.0, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "coin_pool_api_url": "", + "oi_top_api_url": "", + "api_server_port": 8080 +} +``` + +**競争モードの要件:** +- 2つの別々のBinance先物アカウント(異なるAPIキー) +- 両方のAI APIキー(Qwen + DeepSeek) +- テスト用により多くの資本(推奨:アカウントあたり500+ USDT) + +--- + +#### 📚 設定フィールド説明 + +| フィールド | 説明 | 例の値 | 必須? | +|-------|-------------|---------------|-----------| +| `id` | このトレーダーの一意の識別子 | `"my_trader"` | ✅ はい | +| `name` | 表示名 | `"My AI Trader"` | ✅ はい | +| `enabled` | このトレーダーが有効かどうか
起動をスキップする場合は`false`に設定 | `true`または`false` | ✅ はい | +| `ai_model` | 使用するAIプロバイダー | `"deepseek"`または`"qwen"`または`"custom"` | ✅ はい | +| `exchange` | 使用する取引所 | `"binance"`または`"hyperliquid"`または`"aster"` | ✅ はい | +| `binance_api_key` | Binance APIキー | `"abc123..."` | Binance使用時に必須 | +| `binance_secret_key` | Binanceシークレットキー | `"xyz789..."` | Binance使用時に必須 | +| `hyperliquid_private_key` | Hyperliquid秘密鍵
⚠️ `0x`プレフィックスを削除 | `"your_key..."` | Hyperliquid使用時に必須 | +| `hyperliquid_wallet_addr` | Hyperliquidウォレットアドレス | `"0xabc..."` | Hyperliquid使用時に必須 | +| `hyperliquid_testnet` | テストネットを使用 | `true`または`false` | ❌ いいえ(デフォルトはfalse) | +| `use_qwen` | Qwenを使用するかどうか | `true`または`false` | ✅ はい | +| `deepseek_key` | DeepSeek APIキー | `"sk-xxx"` | DeepSeek使用時 | +| `qwen_key` | Qwen APIキー | `"sk-xxx"` | Qwen使用時 | +| `initial_balance` | 損益計算の開始残高 | `1000.0` | ✅ はい | +| `scan_interval_minutes` | 判断を行う頻度 | `3`(3-5推奨) | ✅ はい | +| **`leverage`** | **レバレッジ設定(v2.0.3+)** | 下記参照 | ✅ はい | +| `btc_eth_leverage` | BTC/ETHの最大レバレッジ
⚠️ サブアカウント:≤5x | `5`(デフォルト、安全)
`50`(メインアカウント最大) | ✅ はい | +| `altcoin_leverage` | アルトコインの最大レバレッジ
⚠️ サブアカウント:≤5x | `5`(デフォルト、安全)
`20`(メインアカウント最大) | ✅ はい | +| `use_default_coins` | 組み込みコインリストを使用
**✨ スマートデフォルト:`true`**(v2.0.2+)
API URLが提供されていない場合自動有効化 | `true`または省略 | ❌ いいえ
(オプション、自動デフォルト) | +| `coin_pool_api_url` | カスタムコインプールAPI
*`use_default_coins: false`の場合のみ必要* | `""`(空) | ❌ いいえ | +| `oi_top_api_url` | 建玉API
*オプション補足データ* | `""`(空) | ❌ いいえ | +| `api_server_port` | Webダッシュボードポート | `8080` | ✅ はい | + +**デフォルト取引コイン**(`use_default_coins: true`の場合): +- BTC、ETH、SOL、BNB、XRP、DOGE、ADA、HYPE + +--- + +#### ⚙️ レバレッジ設定(v2.0.3+) + +**レバレッジ設定とは?** + +レバレッジ設定は、AIが各取引で使用できる最大レバレッジを制御します。これは、特にレバレッジ制限があるBinanceサブアカウントでリスク管理に重要です。 + +**設定形式:** + +```json +"leverage": { + "btc_eth_leverage": 5, // BTCとETHの最大レバレッジ + "altcoin_leverage": 5 // その他すべてのコインの最大レバレッジ +} +``` + +**⚠️ 重要:Binanceサブアカウント制限** + +- **サブアカウント**: Binanceにより**≤5xレバレッジ**に制限 +- **メインアカウント**: 最大20x(アルトコイン)または50x(BTC/ETH)を使用可能 +- サブアカウントを使用していてレバレッジを>5xに設定すると、取引は**失敗**し、エラーが表示されます:`Subaccounts are restricted from using leverage greater than 5x` + +**推奨設定:** + +| アカウントタイプ | BTC/ETHレバレッジ | アルトコインレバレッジ | リスクレベル | +|-------------|------------------|------------------|------------| +| **サブアカウント** | `5` | `5` | ✅ 安全(デフォルト) | +| **メイン(保守的)** | `10` | `10` | 🟡 中程度 | +| **メイン(積極的)** | `20` | `15` | 🔴 高 | +| **メイン(最大)** | `50` | `20` | 🔴🔴 非常に高 | + +**例:** + +**安全な設定(サブアカウントまたは保守的):** +```json +"leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 +} +``` + +**積極的な設定(メインアカウントのみ):** +```json +"leverage": { + "btc_eth_leverage": 20, + "altcoin_leverage": 15 +} +``` + +**AIのレバレッジ使用方法:** + +- AIは設定された最大値まで**1xから任意のレバレッジを選択**できます +- たとえば、`altcoin_leverage: 20`の場合、AIは市場条件に基づいて5x、10x、または20xを使用することを決定する可能性があります +- 設定は固定値ではなく**上限**を設定します +- AIはレバレッジを選択する際にボラティリティ、リスクリワード比率、アカウント残高を考慮します + +--- + +#### ⚠️ 重要:`use_default_coins`フィールド + +**スマートデフォルト動作(v2.0.2+):** + +次の場合、システムは自動的に`use_default_coins: true`をデフォルトにします: +- config.jsonにこのフィールドを含めていない、または +- `false`に設定したが`coin_pool_api_url`を提供していない + +これにより初心者に優しくなります!このフィールドを完全に省略することもできます。 + +**設定例:** + +✅ **オプション1:明示的に設定(明確性のため推奨)** +```json +"use_default_coins": true, +"coin_pool_api_url": "", +"oi_top_api_url": "" +``` + +✅ **オプション2:フィールドを省略(デフォルトコインを自動使用)** +```json +// "use_default_coins"を含めないだけ +"coin_pool_api_url": "", +"oi_top_api_url": "" +``` + +⚙️ **高度:外部APIを使用** +```json +"use_default_coins": false, +"coin_pool_api_url": "http://your-api.com/coins", +"oi_top_api_url": "http://your-api.com/oi" +``` + +--- + +### 6. システムを実行 + +#### 🚀 システムの起動(2ステップ) + +システムには別々に実行される**2つの部分**があります: +1. **バックエンド**(AIトレーディングブレイン + API) +2. **フロントエンド**(監視用Webダッシュボード) + +--- + +#### **ステップ1:バックエンドを起動** + +ターミナルを開いて実行: + +```bash +# プログラムをビルド(初回のみ、またはコード変更後) +go build -o nofx + +# バックエンドを起動 +./nofx +``` + +**表示されるべきもの:** + +``` +🚀 启动自动交易系统... +✓ Trader [my_trader] 已初始化 +✓ API服务器启动在端口 8080 +📊 开始交易监控... +``` + +**⚠️ エラーが表示される場合:** + +| エラーメッセージ | 解決策 | +|--------------|----------| +| `invalid API key` | config.jsonのBinance APIキーを確認 | +| `TA-Lib not found` | `brew install ta-lib`を実行(macOS) | +| `port 8080 already in use` | config.jsonの`api_server_port`を変更 | +| `DeepSeek API error` | DeepSeek APIキーと残高を確認 | + +**✅ バックエンドが正しく実行されているとき:** +- エラーメッセージなし +- "开始交易监控..."が表示される +- システムがアカウント残高を表示 +- このターミナルウィンドウを開いたままにしてください! + +--- + +#### **ステップ2:フロントエンドを起動** + +**新しいターミナルウィンドウ**を開き(最初のものは実行したまま)、次を実行: + +```bash +cd web +npm run dev +``` + +**表示されるべきもの:** + +``` +VITE v5.x.x ready in xxx ms + +➜ Local: http://localhost:3000/ +➜ Network: use --host to expose +``` + +**✅ フロントエンドが実行されているとき:** +- "Local: http://localhost:3000/"メッセージ +- エラーメッセージなし +- このターミナルウィンドウも開いたままにしてください! + +--- + +#### **ステップ3:ダッシュボードにアクセス** + +Webブラウザを開いて次にアクセス: + +**🌐 http://localhost:3000** + +**表示されるもの:** +- 📊 リアルタイムアカウント残高 +- 📈 オープンポジション(ある場合) +- 🤖 AI判断ログ +- 📉 エクイティカーブチャート + +**初回のヒント:** +- 最初のAI判断まで3-5分かかることがあります +- 初期判断は「観望」(待機)と言う場合があります - これは正常です +- AIは最初に市場状況を分析する必要があります + +--- + +### 7. システムを監視 + +**監視すべきもの:** + +✅ **健全なシステムの兆候:** +- バックエンドターミナルが3-5分ごとに判断サイクルを表示 +- 継続的なエラーメッセージなし +- アカウント残高の更新 +- Webダッシュボードの自動更新 + +⚠️ **警告の兆候:** +- 繰り返されるAPIエラー +- 10分以上判断なし +- 残高の急速な減少 + +**システムステータスの確認:** + +```bash +# 新しいターミナルウィンドウで +curl http://localhost:8080/health +``` + +戻り値:`{"status":"ok"}` + +--- + +### 8. システムを停止 + +**グレースフルシャットダウン(推奨):** + +1. **バックエンドターミナル**(最初のもの)に移動 +2. `Ctrl+C`を押す +3. "系统已停止"メッセージを待つ +4. **フロントエンドターミナル**(2番目のもの)に移動 +5. `Ctrl+C`を押す + +**⚠️ 重要:** +- 常にバックエンドを最初に停止 +- ターミナルを閉じる前に確認を待つ +- 強制終了しない(ターミナルを直接閉じない) + +--- + +## 📖 AI判断フロー + +各判断サイクル(デフォルト3分)で、システムは以下のインテリジェントプロセスを実行します: + +``` +┌──────────────────────────────────────────────────────────┐ +│ 1. 📊 過去パフォーマンスを分析(過去20サイクル) │ +├──────────────────────────────────────────────────────────┤ +│ ✓ 総合勝率、平均利益、損益比を計算 │ +│ ✓ コインごとの統計(勝率、平均損益(USDT)) │ +│ ✓ 最高/最悪パフォーマンスコインを特定 │ +│ ✓ 正確なPnLを含む最後の5取引の詳細をリスト │ +│ ✓ リスク調整パフォーマンスのシャープレシオを計算 │ +│ 📌 NEW(v2.0.2):レバレッジを含む正確なUSDT PnL │ +└──────────────────────────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 2. 💰 アカウントステータスを取得 │ +├──────────────────────────────────────────────────────────┤ +│ • 総エクイティと利用可能残高 │ +│ • オープンポジション数と未実現損益 │ +│ • 証拠金使用率(AIは最大90%を管理) │ +│ • 日次損益追跡とドローダウン監視 │ +└──────────────────────────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 3. 🔍 既存ポジションを分析(ある場合) │ +├──────────────────────────────────────────────────────────┤ +│ • 各ポジションについて、最新の市場データを取得 │ +│ • リアルタイムのテクニカル指標を計算: │ +│ - 3分K線:RSI(7)、MACD、EMA20 │ +│ - 4時間K線:RSI(14)、EMA20/50、ATR │ +│ • ポジション保有期間を追跡(例:「2時間15分」) │ +│ 📌 NEW(v2.0.2):各ポジションの保有期間を表示 │ +│ • 表示:エントリー価格、現在価格、損益%、期間 │ +│ • AIが評価:保持するかクローズするか? │ +└──────────────────────────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 4. 🎯 新しい機会を評価(候補コイン) │ +├──────────────────────────────────────────────────────────┤ +│ • コインプールを取得(2モード): │ +│ 🌟 デフォルトモード:BTC、ETH、SOL、BNB、XRPなど │ +│ ⚙️ 高度モード:AI500(上位20)+ OI Top(上位20) │ +│ • 候補コインをマージして重複削除 │ +│ • フィルター:低流動性を削除(<1500万USD OI値) │ +│ • 市場データ + テクニカル指標をバッチ取得 │ +│ • ボラティリティ、トレンド強度、出来高急増を計算 │ +└──────────────────────────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 5. 🧠 AI総合判断(DeepSeek/Qwen) │ +├──────────────────────────────────────────────────────────┤ +│ • 過去フィードバックをレビュー: │ +│ - 最近の勝率と利益率 │ +│ - 最高/最悪コインパフォーマンス │ +│ - 繰り返しミスを回避 │ +│ • すべての生シーケンスデータを分析: │ +│ - 3分価格シーケンス、4時間K線シーケンス │ +│ - 完全な指標シーケンス(最新のみではない) │ +│ 📌 NEW(v2.0.2):AIは分析の完全な自由を持つ │ +│ • 思考連鎖(CoT)推論プロセス │ +│ • 構造化された判断を出力: │ +│ - アクション:close_long/close_short/open_long/open_short│ +│ - コインシンボル、数量、レバレッジ │ +│ - ストップロスとテイクプロフィットレベル(≥1:2比率) │ +│ • 判断:待機/保持/クローズ/オープン │ +└──────────────────────────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 6. ⚡ 取引を実行 │ +├──────────────────────────────────────────────────────────┤ +│ • 優先順位:既存をクローズ → その後新規をオープン │ +│ • 実行前のリスクチェック: │ +│ - ポジションサイズ制限(アルトコイン1.5x、BTC 10x) │ +│ - 重複ポジションなし(同じコイン + 方向) │ +│ - 証拠金使用量が90%制限内 │ +│ • Binance LOT_SIZE精度を自動取得して適用 │ +│ • Binance Futures APIで注文を実行 │ +│ • クローズ後:すべての保留注文を自動キャンセル │ +│ • 実際の実行価格と注文IDを記録 │ +│ 📌 期間計算のためにポジションオープン時間を追跡 │ +└──────────────────────────────────────────────────────────┘ + ↓ +┌──────────────────────────────────────────────────────────┐ +│ 7. 📝 完全なログを記録してパフォーマンスを更新 │ +├──────────────────────────────────────────────────────────┤ +│ • decision_logs/{trader_id}/に判断ログを保存 │ +│ • ログには以下が含まれます: │ +│ - 完全な思考連鎖(CoT) │ +│ - すべての市場データを含む入力プロンプト │ +│ - 構造化された判断JSON │ +│ - アカウントスナップショット(残高、ポジション、証拠金)│ +│ - 実行結果(成功/失敗、価格) │ +│ • パフォーマンスデータベースを更新: │ +│ - symbol_sideキーでオープン/クローズペアをマッチ │ +│ 📌 NEW:ロング/ショート競合を防止 │ +│ - 正確なUSDT PnLを計算: │ +│ PnL = ポジション価値 × 価格変化% × レバレッジ │ +│ 📌 NEW:数量 + レバレッジを考慮 │ +│ - 保存:数量、レバレッジ、オープン時間、クローズ時間 │ +│ - 更新:勝率、利益率、シャープレシオ │ +│ • パフォーマンスデータは次のサイクルにフィードバック │ +└──────────────────────────────────────────────────────────┘ + ↓ + (3-5分ごとに繰り返し) +``` + +### v2.0.2の主な改善点 + +**📌 ポジション期間追跡:** +- システムが各ポジションの保有期間を追跡 +- ユーザープロンプトに表示:「持仓时长2小时15分钟」 +- AIが出口タイミングについてより良い判断を下すのに役立つ + +**📌 正確なPnL計算:** +- 以前:パーセンテージのみ(100U@5% = 1000U@5% = 両方とも「5.0」と表示) +- 現在:実際のUSDT利益 = ポジション価値 × 価格変化 × レバレッジ +- 例:1000 USDT × 5% × 20x = 1000 USDT実際の利益 + +**📌 AI自由度の向上:** +- AIはすべての生シーケンスデータを自由に分析可能 +- 事前定義された指標の組み合わせに制限されない +- 独自のトレンド分析、サポート/レジスタンス計算を実行可能 + +**📌 改善されたポジション追跡:** +- `symbol_side`キーを使用(例:「BTCUSDT_long」) +- ロングとショートの両方を保有する際の競合を防止 +- 完全なデータを保存:数量、レバレッジ、オープン/クローズ時間 + +--- + +## 🧠 AI自己学習の例 + +### 過去フィードバック(プロンプトに自動追加) + +```markdown +## 📊 過去パフォーマンスフィードバック + +### 総合パフォーマンス +- **総取引数**: 15(利益:8 | 損失:7) +- **勝率**: 53.3% +- **平均利益**: +3.2% | 平均損失:-2.1% +- **損益比**: 1.52:1 + +### 最近の取引 +1. BTCUSDT LONG: 95000.0000 → 97500.0000 = +2.63% ✓ +2. ETHUSDT SHORT: 3500.0000 → 3450.0000 = +1.43% ✓ +3. SOLUSDT LONG: 185.0000 → 180.0000 = -2.70% ✗ +4. BNBUSDT LONG: 610.0000 → 625.0000 = +2.46% ✓ +5. ADAUSDT LONG: 0.8500 → 0.8300 = -2.35% ✗ + +### コインパフォーマンス +- **最高**: BTCUSDT(勝率75%、平均+2.5%) +- **最悪**: SOLUSDT(勝率25%、平均-1.8%) +``` + +### AIのフィードバック使用方法 + +1. **連続損失を回避**: SOLUSDTが3回連続でストップロスになっているのを見て、AIは回避するかより慎重になる +2. **成功戦略を強化**: BTCブレイクアウトロングが75%の勝率で、AIはこのパターンを継続 +3. **動的スタイル調整**: 勝率<40% → 保守的;損益比>2 → 積極的を維持 +4. **市場状況の特定**: 連続損失は荒れた市場を示す可能性があり、取引頻度を減らす + +--- + +## 📊 Webインターフェース機能 + +### 1. 競争ページ + +- **🏆 リーダーボード**: リアルタイムROIランキング、ゴールドボーダーでリーダーをハイライト +- **📈 パフォーマンス比較**: デュアルAI ROIカーブ比較(紫対青) +- **⚔️ 一対一**: リードマージンを示す直接比較 +- **リアルタイムデータ**: 総エクイティ、損益%、ポジション数、証拠金使用量 + +### 2. 詳細ページ + +- **エクイティカーブ**: 過去トレンドチャート(USD/パーセンテージ切り替え) +- **統計**: 総サイクル、成功/失敗、オープン/クローズ統計 +- **ポジションテーブル**: すべてのポジション詳細(エントリー価格、現在価格、損益%、清算価格) +- **AI判断ログ**: 最近の判断記録(展開可能なCoT) + +### 3. リアルタイム更新 + +- システムステータス、アカウント情報、ポジションリスト:**5秒更新** +- 判断ログ、統計:**10秒更新** +- エクイティチャート:**10秒更新** + +--- + +## 🎛️ APIエンドポイント + +### 競争関連 + +```bash +GET /api/competition # 競争リーダーボード(全トレーダー) +GET /api/traders # トレーダーリスト +``` + +### 単一トレーダー関連 + +```bash +GET /api/status?trader_id=xxx # システムステータス +GET /api/account?trader_id=xxx # アカウント情報 +GET /api/positions?trader_id=xxx # ポジションリスト +GET /api/equity-history?trader_id=xxx # エクイティ履歴(チャートデータ) +GET /api/decisions/latest?trader_id=xxx # 最新5判断 +GET /api/statistics?trader_id=xxx # 統計 +``` + +### システムエンドポイント + +```bash +GET /health # ヘルスチェック +GET /api/config # システム設定 +``` + +--- + +## ⚠️ 重要なリスク警告 + +### 取引リスク + +1. **暗号通貨市場は非常にボラティルが高い**、AI判断は利益を保証しません +2. **先物取引はレバレッジを使用**、損失が元本を超える可能性があります +3. **極端な市場状況**は清算リスクにつながる可能性があります +4. **ファンディングレート**は保有コストに影響する可能性があります +5. **流動性リスク**: 一部のコインでスリッページが発生する可能性があります + +### 技術リスク + +1. **ネットワークレイテンシ**は価格スリッページを引き起こす可能性があります +2. **APIレート制限**は取引実行に影響する可能性があります +3. **AI APIタイムアウト**は判断失敗を引き起こす可能性があります +4. **システムバグ**は予期しない動作を引き起こす可能性があります + +### 使用推奨事項 + +✅ **推奨** +- テストには失っても構わない資金のみを使用 +- 少額から始める(推奨100-500 USDT) +- システムの動作状態を定期的に確認 +- アカウント残高の変化を監視 +- AI判断ログを分析して戦略を理解 + +❌ **非推奨** +- すべての資金または借りたお金を投資 +- 長期間監視なしで実行 +- AI判断を盲目的に信頼 +- システムを理解せずに使用 +- 極端な市場ボラティリティ中に実行 + +--- + +## 🛠️ よくある問題 + +### 1. コンパイルエラー:TA-Libが見つからない + +**解決策**: TA-Libライブラリをインストール +```bash +# macOS +brew install ta-lib + +# Ubuntu +sudo apt-get install libta-lib0-dev +``` + +### 2. 精度エラー:Precision is over the maximum + +**解決策**: システムがBinance LOT_SIZEから精度を自動処理します。エラーが続く場合は、ネットワーク接続を確認してください。 + +### 3. AI APIタイムアウト + +**解決策**: +- APIキーが正しいか確認 +- ネットワーク接続を確認(プロキシが必要な場合があります) +- システムタイムアウトは120秒に設定されています + +### 4. フロントエンドがバックエンドに接続できない + +**解決策**: +- バックエンドが実行中であることを確認(http://localhost:8080) +- ポート8080が占有されていないか確認 +- ブラウザコンソールでエラーを確認 + +### 5. コインプールAPI失敗 + +**解決策**: +- コインプールAPIはオプションです +- APIが失敗した場合、システムはデフォルトのメインストリームコイン(BTC、ETHなど)を使用 +- config.jsonのAPI URLと認証パラメータを確認 + +--- + +## 📈 パフォーマンス最適化のヒント + +1. **合理的な判断サイクルを設定**: 3-5分を推奨、過剰取引を避ける +2. **候補コイン数を制御**: システムはデフォルトでAI500上位20 + OI Top上位20 +3. **ログを定期的にクリーン**: 過度なディスク使用を避ける +4. **API呼び出し数を監視**: Binanceレート制限のトリガーを避ける +5. **少額資本でテスト**: まず100-500 USDTで戦略検証をテスト + +--- + +## 🔄 変更履歴 + +### v2.0.2(2025-10-29) + +**重大なバグ修正 - 取引履歴とパフォーマンス分析:** + +このバージョンは、収益性統計に大きく影響した過去取引記録とパフォーマンス分析システムの**重大な計算エラー**を修正します。 + +**1. PnL計算 - 主要エラー修正**(logger/decision_logger.go) +- **問題**: 以前はパーセンテージのみで計算され、ポジションサイズとレバレッジを完全に無視 + - 例:100 USDTポジションが5%獲得と1000 USDTポジションが5%獲得の両方が利益として`5.0`と表示 + - これによりパフォーマンス分析が完全に不正確に +- **解決策**: 実際のUSDT利益額を計算 + ``` + PnL(USDT)= ポジション価値 × 価格変化% × レバレッジ + 例:1000 USDT × 5% × 20x = 1000 USDT実際の利益 + ``` +- **影響**: 勝率、利益率、シャープレシオが正確なUSDT額に基づくようになりました + +**2. ポジション追跡 - 重要データの欠落** +- **問題**: オープンポジション記録が価格と時間のみを保存、数量とレバレッジが欠落 +- **解決策**: 完全な取引データを保存: + - `quantity`: ポジションサイズ(コイン単位) + - `leverage`: レバレッジ倍率(例:20x) + - これらは正確なPnL計算に不可欠 + +**3. ポジションキーロジック - ロング/ショート競合** +- **問題**: `symbol`をポジションキーとして使用し、ロングとショートの両方を保有する際にデータ競合を引き起こす + - 例:BTCUSDTロングとBTCUSDTショートが互いに上書き +- **解決策**: `symbol_side`形式に変更(例:`BTCUSDT_long`、`BTCUSDT_short`) + - ロングとショートポジションを適切に区別 + +**4. シャープレシオ計算 - コード最適化** +- **問題**: 平方根計算にカスタムニュートン法を使用 +- **解決策**: 標準ライブラリ`math.Sqrt`に置き換え + - より信頼性が高く、保守可能で効率的 + +**このアップデートが重要な理由:** +- ✅ 過去取引統計が無意味なパーセンテージではなく**実際のUSDT損益**を表示 +- ✅ 異なるレバレッジ取引間のパフォーマンス比較が正確に +- ✅ AI自己学習メカニズムが正しい過去フィードバックを受信 +- ✅ 利益率とシャープレシオの計算が意味を持つように +- ✅ マルチポジション追跡(ロング + ショート同時)が正しく機能 + +**推奨**: このアップデート前にシステムを実行していた場合、過去統計は不正確でした。v2.0.2にアップデート後、新しい取引は正しく計算されます。 + +### v2.0.2(2025-10-29) + +**バグ修正:** +- ✅ Aster取引所精度エラーを修正(コード-1111:「Precision is over the maximum defined for this asset」) +- ✅ 取引所の精度要件に合わせて価格と数量のフォーマットを改善 +- ✅ デバッグ用の詳細な精度処理ログを追加 +- ✅ 適切な精度処理ですべての注文関数(OpenLong、OpenShort、CloseLong、CloseShort、SetStopLoss、SetTakeProfit)を強化 + +**技術詳細:** +- float64を正しい精度で文字列に変換する`formatFloatWithPrecision`関数を追加 +- 価格と数量パラメータが取引所の`pricePrecision`と`quantityPrecision`仕様に従ってフォーマットされるようになりました +- API リクエストを最適化するために、フォーマットされた値から末尾のゼロを削除 + +### v2.0.1(2025-10-29) + +**バグ修正:** +- ✅ ComparisonChartデータ処理ロジックを修正 - cycle_numberからタイムスタンプグループ化に切り替え +- ✅ バックエンド再起動時にcycle_numberがリセットされるとチャートがフリーズする問題を解決 +- ✅ チャートデータ表示を改善 - すべての過去データポイントを時系列で表示 +- ✅ トラブルシューティングを改善するためのデバッグログを強化 + +### v2.0.0(2025-10-28) + +**主要アップデート:** +- ✅ AI自己学習メカニズム(過去フィードバック、パフォーマンス分析) +- ✅ マルチトレーダー競争モード(Qwen対DeepSeek) +- ✅ BinanceスタイルUI(完全なBinanceインターフェース模倣) +- ✅ パフォーマンス比較チャート(リアルタイムROI比較) +- ✅ リスク管理最適化(コインごとのポジション制限調整) + +**バグ修正:** +- 初期残高のハードコーディング問題を修正 +- マルチトレーダーデータ同期問題を修正 +- チャートデータの整列を最適化(cycle_numberを使用) + +### v1.0.0(2025-10-27) +- 初回リリース +- 基本的なAI取引機能 +- 判断ログシステム +- シンプルなWebインターフェース + +--- + +## 📄 ライセンス + +MITライセンス - 詳細は[LICENSE](LICENSE)ファイルを参照してください + +--- + +## 🤝 貢献 + +IssueとPull Requestを歓迎します! + +### 開発ガイド + +1. プロジェクトをフォーク +2. 機能ブランチを作成(`git checkout -b feature/AmazingFeature`) +3. 変更をコミット(`git commit -m 'Add some AmazingFeature'`) +4. ブランチにプッシュ(`git push origin feature/AmazingFeature`) +5. Pull Requestを開く + +--- + +## 📬 お問い合わせ + + +### 🐛 技術サポート +- **GitHub Issues**: [Issueを提出](https://github.com/tinkle-community/nofx/issues) +- **開発者コミュニティ**: [Telegramグループ](https://t.me/nofx_dev_community) + +--- + +## 🙏 謝辞 + +- [Binance API](https://binance-docs.github.io/apidocs/futures/en/) - Binance先物API +- [DeepSeek](https://platform.deepseek.com/) - DeepSeek AI API +- [Qwen](https://dashscope.aliyuncs.com/) - Alibaba Cloud Qwen +- [TA-Lib](https://ta-lib.org/) - テクニカル指標ライブラリ +- [Recharts](https://recharts.org/) - Reactチャートライブラリ + +--- + +**最終更新**: 2025-10-29(v2.0.3) + +**⚡ AIの力で量的取引の可能性を探求しましょう!** + +--- + +## ⭐ Star履歴 + +[![Star履歴チャート](https://api.star-history.com/svg?repos=tinkle-community/nofx&type=Date)](https://star-history.com/#tinkle-community/nofx&Date) diff --git a/README.md b/README.md index f76b9067..4a82bfed 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) [![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) -[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE) [![Backed by Amber.ac](https://img.shields.io/badge/Backed%20by-Amber.ac-orange.svg)](https://amber.ac) **Languages:** [English](README.md) | [中文](docs/i18n/zh-CN/README.md) | [Українська](docs/i18n/uk/README.md) | [Русский](docs/i18n/ru/README.md) @@ -281,7 +281,7 @@ Docker automatically handles all dependencies (Go, Node.js, TA-Lib, SQLite) and #### Step 1: Prepare Configuration ```bash # Copy configuration template -cp config.example.jsonc config.json +cp config.json.example config.json # Edit and fill in your API keys nano config.json # or use any editor @@ -1240,7 +1240,15 @@ sudo apt-get install libta-lib0-dev ## 📄 License -MIT License - See [LICENSE](LICENSE) file for details +This project is licensed under the **GNU Affero General Public License v3.0 (AGPL-3.0)** - See [LICENSE](LICENSE) file for details. + +**What this means:** +- ✅ You can use, modify, and distribute this software +- ✅ You must disclose source code of your modifications +- ✅ If you run a modified version on a server, you must make the source code available to users +- ✅ All derivatives must also be licensed under AGPL-3.0 + +For commercial licensing or questions, please contact the maintainers. --- diff --git a/api/server.go b/api/server.go index bb0a6eaf..b068d5aa 100644 --- a/api/server.go +++ b/api/server.go @@ -4,11 +4,13 @@ import ( "encoding/json" "fmt" "log" + "net" "net/http" "nofx/auth" "nofx/config" "nofx/decision" "nofx/manager" + "nofx/trader" "strconv" "strings" "time" @@ -88,7 +90,7 @@ func (s *Server) setupRoutes() { // 系统提示词模板管理(无需认证) api.GET("/prompt-templates", s.handleGetPromptTemplates) api.GET("/prompt-templates/:name", s.handleGetPromptTemplate) - + // 公开的竞赛数据(无需认证) api.GET("/traders", s.handlePublicTraderList) api.GET("/competition", s.handlePublicCompetition) @@ -100,6 +102,9 @@ func (s *Server) setupRoutes() { // 需要认证的路由 protected := api.Group("/", s.authMiddleware()) { + // 服务器IP查询(需要认证,用于白名单配置) + protected.GET("/server-ip", s.handleGetServerIP) + // AI交易员管理 protected.GET("/my-traders", s.handleTraderList) protected.GET("/traders/:id/config", s.handleGetTraderConfig) @@ -109,6 +114,7 @@ func (s *Server) setupRoutes() { protected.POST("/traders/:id/start", s.handleStartTrader) protected.POST("/traders/:id/stop", s.handleStopTrader) protected.PUT("/traders/:id/prompt", s.handleUpdateTraderPrompt) + protected.POST("/traders/:id/sync-balance", s.handleSyncBalance) // AI模型配置 protected.GET("/models", s.handleGetModelConfigs) @@ -168,7 +174,7 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { if val, err := strconv.Atoi(altcoinLeverageStr); err == nil && val > 0 { altcoinLeverage = val } - + // 获取内测模式配置 betaModeStr, _ := s.database.GetSystemConfig("beta_mode") betaMode := betaModeStr == "true" @@ -182,6 +188,133 @@ func (s *Server) handleGetSystemConfig(c *gin.Context) { }) } +// handleGetServerIP 获取服务器IP地址(用于白名单配置) +func (s *Server) handleGetServerIP(c *gin.Context) { + // 尝试通过第三方API获取公网IP + publicIP := getPublicIPFromAPI() + + // 如果第三方API失败,从网络接口获取第一个公网IP + if publicIP == "" { + publicIP = getPublicIPFromInterface() + } + + // 如果还是没有获取到,返回错误 + if publicIP == "" { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取公网IP地址"}) + return + } + + c.JSON(http.StatusOK, gin.H{ + "public_ip": publicIP, + "message": "请将此IP地址添加到白名单中", + }) +} + +// getPublicIPFromAPI 通过第三方API获取公网IP +func getPublicIPFromAPI() string { + // 尝试多个公网IP查询服务 + services := []string{ + "https://api.ipify.org?format=text", + "https://icanhazip.com", + "https://ifconfig.me", + } + + client := &http.Client{ + Timeout: 5 * time.Second, + } + + for _, service := range services { + resp, err := client.Get(service) + if err != nil { + continue + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + body := make([]byte, 128) + n, err := resp.Body.Read(body) + if err != nil && err.Error() != "EOF" { + continue + } + + ip := strings.TrimSpace(string(body[:n])) + // 验证是否为有效的IP地址 + if net.ParseIP(ip) != nil { + return ip + } + } + } + + return "" +} + +// getPublicIPFromInterface 从网络接口获取第一个公网IP +func getPublicIPFromInterface() string { + interfaces, err := net.Interfaces() + if err != nil { + return "" + } + + for _, iface := range interfaces { + // 跳过未启用的接口和回环接口 + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + + addrs, err := iface.Addrs() + if err != nil { + continue + } + + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + + if ip == nil || ip.IsLoopback() { + continue + } + + // 只考虑IPv4地址 + if ip.To4() != nil { + ipStr := ip.String() + // 排除私有IP地址范围 + if !isPrivateIP(ip) { + return ipStr + } + } + } + } + + return "" +} + +// isPrivateIP 判断是否为私有IP地址 +func isPrivateIP(ip net.IP) bool { + // 私有IP地址范围: + // 10.0.0.0/8 + // 172.16.0.0/12 + // 192.168.0.0/16 + privateRanges := []string{ + "10.0.0.0/8", + "172.16.0.0/12", + "192.168.0.0/16", + } + + for _, cidr := range privateRanges { + _, subnet, _ := net.ParseCIDR(cidr) + if subnet.Contains(ip) { + return true + } + } + + return false +} + // getTraderFromQuery 从query参数获取trader func (s *Server) getTraderFromQuery(c *gin.Context) (*manager.TraderManager, string, error) { userID := c.GetString("user_id") @@ -343,8 +476,75 @@ func (s *Server) handleCreateTrader(c *gin.Context) { // 设置扫描间隔默认值 scanIntervalMinutes := req.ScanIntervalMinutes - if scanIntervalMinutes <= 0 { - scanIntervalMinutes = 3 // 默认3分钟 + if scanIntervalMinutes < 3 { + scanIntervalMinutes = 3 // 默认3分钟,且不允许小于3 + } + + // ✨ 查询交易所实际余额,覆盖用户输入 + actualBalance := req.InitialBalance // 默认使用用户输入 + exchanges, err := s.database.GetExchanges(userID) + if err != nil { + log.Printf("⚠️ 获取交易所配置失败,使用用户输入的初始资金: %v", err) + } + + // 查找匹配的交易所配置 + var exchangeCfg *config.ExchangeConfig + for _, ex := range exchanges { + if ex.ID == req.ExchangeID { + exchangeCfg = ex + break + } + } + + if exchangeCfg == nil { + log.Printf("⚠️ 未找到交易所 %s 的配置,使用用户输入的初始资金", req.ExchangeID) + } else if !exchangeCfg.Enabled { + log.Printf("⚠️ 交易所 %s 未启用,使用用户输入的初始资金", req.ExchangeID) + } else { + // 根据交易所类型创建临时 trader 查询余额 + var tempTrader trader.Trader + var createErr error + + switch req.ExchangeID { + case "binance": + tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey) + case "hyperliquid": + tempTrader, createErr = trader.NewHyperliquidTrader( + exchangeCfg.APIKey, // private key + exchangeCfg.HyperliquidWalletAddr, + exchangeCfg.Testnet, + ) + case "aster": + tempTrader, createErr = trader.NewAsterTrader( + exchangeCfg.AsterUser, + exchangeCfg.AsterSigner, + exchangeCfg.AsterPrivateKey, + ) + default: + log.Printf("⚠️ 不支持的交易所类型: %s,使用用户输入的初始资金", req.ExchangeID) + } + + if createErr != nil { + log.Printf("⚠️ 创建临时 trader 失败,使用用户输入的初始资金: %v", createErr) + } else if tempTrader != nil { + // 查询实际余额 + balanceInfo, balanceErr := tempTrader.GetBalance() + if balanceErr != nil { + log.Printf("⚠️ 查询交易所余额失败,使用用户输入的初始资金: %v", balanceErr) + } else { + // 提取可用余额 + if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { + // 有些交易所可能只返回 balance 字段 + actualBalance = totalBalance + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (用户输入: %.2f USDT)", actualBalance, req.InitialBalance) + } else { + log.Printf("⚠️ 无法从余额信息中提取可用余额,使用用户输入的初始资金") + } + } + } } // 创建交易员配置(数据库实体) @@ -354,7 +554,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { Name: req.Name, AIModelID: req.AIModelID, ExchangeID: req.ExchangeID, - InitialBalance: req.InitialBalance, + InitialBalance: actualBalance, // 使用实际查询的余额 BTCETHLeverage: btcEthLeverage, AltcoinLeverage: altcoinLeverage, TradingSymbols: req.TradingSymbols, @@ -369,7 +569,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } // 保存到数据库 - err := s.database.CreateTrader(trader) + err = s.database.CreateTrader(trader) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) return @@ -458,6 +658,8 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { scanIntervalMinutes := req.ScanIntervalMinutes if scanIntervalMinutes <= 0 { scanIntervalMinutes = existingTrader.ScanIntervalMinutes // 保持原值 + } else if scanIntervalMinutes < 3 { + scanIntervalMinutes = 3 } // 更新交易员配置 @@ -531,14 +733,14 @@ func (s *Server) handleDeleteTrader(c *gin.Context) { func (s *Server) handleStartTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - + // 校验交易员是否属于当前用户 _, _, _, err := s.database.GetTraderConfig(userID, traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) return } - + trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) @@ -574,14 +776,14 @@ func (s *Server) handleStartTrader(c *gin.Context) { func (s *Server) handleStopTrader(c *gin.Context) { userID := c.GetString("user_id") traderID := c.Param("id") - + // 校验交易员是否属于当前用户 _, _, _, err := s.database.GetTraderConfig(userID, traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在或无访问权限"}) return } - + trader, err := s.traderManager.GetTrader(traderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) @@ -641,6 +843,113 @@ func (s *Server) handleUpdateTraderPrompt(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "自定义prompt已更新"}) } +// handleSyncBalance 同步交易所余额到initial_balance(选项B:手动同步 + 选项C:智能检测) +func (s *Server) handleSyncBalance(c *gin.Context) { + userID := c.GetString("user_id") + traderID := c.Param("id") + + log.Printf("🔄 用户 %s 请求同步交易员 %s 的余额", userID, traderID) + + // 从数据库获取交易员配置(包含交易所信息) + traderConfig, _, exchangeCfg, err := s.database.GetTraderConfig(userID, traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "交易员不存在"}) + return + } + + if exchangeCfg == nil || !exchangeCfg.Enabled { + c.JSON(http.StatusBadRequest, gin.H{"error": "交易所未配置或未启用"}) + return + } + + // 创建临时 trader 查询余额 + var tempTrader trader.Trader + var createErr error + + switch traderConfig.ExchangeID { + case "binance": + tempTrader = trader.NewFuturesTrader(exchangeCfg.APIKey, exchangeCfg.SecretKey) + case "hyperliquid": + tempTrader, createErr = trader.NewHyperliquidTrader( + exchangeCfg.APIKey, + exchangeCfg.HyperliquidWalletAddr, + exchangeCfg.Testnet, + ) + case "aster": + tempTrader, createErr = trader.NewAsterTrader( + exchangeCfg.AsterUser, + exchangeCfg.AsterSigner, + exchangeCfg.AsterPrivateKey, + ) + default: + c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的交易所类型"}) + return + } + + if createErr != nil { + log.Printf("⚠️ 创建临时 trader 失败: %v", createErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("连接交易所失败: %v", createErr)}) + return + } + + // 查询实际余额 + balanceInfo, balanceErr := tempTrader.GetBalance() + if balanceErr != nil { + log.Printf("⚠️ 查询交易所余额失败: %v", balanceErr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("查询余额失败: %v", balanceErr)}) + return + } + + // 提取可用余额 + var actualBalance float64 + if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { + actualBalance = totalBalance + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取可用余额"}) + return + } + + oldBalance := traderConfig.InitialBalance + + // ✅ 选项C:智能检测余额变化 + changePercent := ((actualBalance - oldBalance) / oldBalance) * 100 + changeType := "增加" + if changePercent < 0 { + changeType = "减少" + } + + log.Printf("✓ 查询到交易所实际余额: %.2f USDT (当前配置: %.2f USDT, 变化: %.2f%%)", + actualBalance, oldBalance, changePercent) + + // 更新数据库中的 initial_balance + err = s.database.UpdateTraderInitialBalance(userID, traderID, actualBalance) + if err != nil { + log.Printf("❌ 更新initial_balance失败: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "更新余额失败"}) + return + } + + // 重新加载交易员到内存 + err = s.traderManager.LoadUserTraders(s.database, userID) + if err != nil { + log.Printf("⚠️ 重新加载用户交易员到内存失败: %v", err) + } + + log.Printf("✅ 已同步余额: %.2f → %.2f USDT (%s %.2f%%)", oldBalance, actualBalance, changeType, changePercent) + + c.JSON(http.StatusOK, gin.H{ + "message": "余额同步成功", + "old_balance": oldBalance, + "new_balance": actualBalance, + "change_percent": changePercent, + "change_type": changeType, + }) +} + // handleGetModelConfigs 获取AI模型配置 func (s *Server) handleGetModelConfigs(c *gin.Context) { userID := c.GetString("user_id") @@ -791,19 +1100,12 @@ func (s *Server) handleTraderList(c *gin.Context) { } } - // AIModelID 应该已经是 provider(如 "deepseek"),直接使用 - // 如果是旧数据格式(如 "admin_deepseek"),提取 provider 部分 - aiModelID := trader.AIModelID - // 兼容旧数据:如果包含下划线,提取最后一部分作为 provider - if strings.Contains(aiModelID, "_") { - parts := strings.Split(aiModelID, "_") - aiModelID = parts[len(parts)-1] - } - + // 返回完整的 AIModelID(如 "admin_deepseek"),不要截断 + // 前端需要完整 ID 来验证模型是否存在(与 handleGetTraderConfig 保持一致) result = append(result, map[string]interface{}{ "trader_id": trader.ID, "trader_name": trader.Name, - "ai_model": aiModelID, + "ai_model": trader.AIModelID, // 使用完整 ID "exchange_id": trader.ExchangeID, "is_running": isRunning, "initial_balance": trader.InitialBalance, @@ -1363,7 +1665,6 @@ func (s *Server) handleLogin(c *gin.Context) { // 验证密码 if !auth.CheckPassword(req.Password, user.PasswordHash) { - log.Printf("DEBUG: 密码验证失败") c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) return } @@ -1582,7 +1883,7 @@ func (s *Server) handlePublicCompetition(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, competition) } @@ -1595,7 +1896,7 @@ func (s *Server) handleTopTraders(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, topTraders) } @@ -1604,7 +1905,7 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { var requestBody struct { TraderIDs []string `json:"trader_ids"` } - + // 尝试解析POST请求的JSON body if err := c.ShouldBindJSON(&requestBody); err != nil { // 如果JSON解析失败,尝试从query参数获取(兼容GET请求) @@ -1618,13 +1919,13 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { }) return } - + traders, ok := topTraders["traders"].([]map[string]interface{}) if !ok { c.JSON(http.StatusInternalServerError, gin.H{"error": "交易员数据格式错误"}) return } - + // 提取trader IDs traderIDs := make([]string, 0, len(traders)) for _, trader := range traders { @@ -1632,24 +1933,24 @@ func (s *Server) handleEquityHistoryBatch(c *gin.Context) { traderIDs = append(traderIDs, traderID) } } - + result := s.getEquityHistoryForTraders(traderIDs) c.JSON(http.StatusOK, result) return } - + // 解析逗号分隔的trader IDs requestBody.TraderIDs = strings.Split(traderIDsParam, ",") for i := range requestBody.TraderIDs { requestBody.TraderIDs[i] = strings.TrimSpace(requestBody.TraderIDs[i]) } } - + // 限制最多20个交易员,防止请求过大 if len(requestBody.TraderIDs) > 20 { requestBody.TraderIDs = requestBody.TraderIDs[:20] } - + result := s.getEquityHistoryForTraders(requestBody.TraderIDs) c.JSON(http.StatusOK, result) } @@ -1659,31 +1960,31 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter result := make(map[string]interface{}) histories := make(map[string]interface{}) errors := make(map[string]string) - + for _, traderID := range traderIDs { if traderID == "" { continue } - + trader, err := s.traderManager.GetTrader(traderID) if err != nil { errors[traderID] = "交易员不存在" continue } - + // 获取历史数据(用于对比展示,限制数据量) records, err := trader.GetDecisionLogger().GetLatestRecords(500) if err != nil { errors[traderID] = fmt.Sprintf("获取历史数据失败: %v", err) continue } - + // 构建收益率历史数据 history := make([]map[string]interface{}, 0, len(records)) for _, record := range records { // 计算总权益(余额+未实现盈亏) totalEquity := record.AccountState.TotalBalance + record.AccountState.TotalUnrealizedProfit - + history = append(history, map[string]interface{}{ "timestamp": record.Timestamp, "total_equity": totalEquity, @@ -1691,16 +1992,16 @@ func (s *Server) getEquityHistoryForTraders(traderIDs []string) map[string]inter "balance": record.AccountState.TotalBalance, }) } - + histories[traderID] = history } - + result["histories"] = histories result["count"] = len(histories) if len(errors) > 0 { result["errors"] = errors } - + return result } @@ -1734,4 +2035,3 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { c.JSON(http.StatusOK, result) } - diff --git a/config.json.example b/config.json.example index fefa1673..820f39a7 100644 --- a/config.json.example +++ b/config.json.example @@ -20,5 +20,8 @@ "max_daily_loss": 10.0, "max_drawdown": 20.0, "stop_trading_minutes": 60, - "jwt_secret": "Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==" + "jwt_secret": "Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==", + "log": { + "level": "info" + } } \ No newline at end of file diff --git a/config/config.go b/config/config.go index 37a537db..b913212f 100644 --- a/config/config.go +++ b/config/config.go @@ -50,6 +50,20 @@ type LeverageConfig struct { AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币的杠杆倍数(主账户建议5-20,子账户≤5) } +// LogConfig 日志配置 +type LogConfig struct { + Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info) + Telegram *TelegramConfig `json:"telegram"` // Telegram推送配置(可选) +} + +// TelegramConfig Telegram推送配置(简化版,只保留必需字段) +type TelegramConfig struct { + Enabled bool `json:"enabled"` // 是否启用(默认: false) + BotToken string `json:"bot_token"` // Bot Token + ChatID int64 `json:"chat_id"` // Chat ID + MinLevel string `json:"min_level"` // 最低日志级别,该级别及以上的日志会推送到Telegram(可选,默认: error) +} + // Config 总配置 type Config struct { Traders []TraderConfig `json:"traders"` @@ -60,6 +74,7 @@ type Config struct { MaxDrawdown float64 `json:"max_drawdown"` StopTradingMinutes int `json:"stop_trading_minutes"` Leverage LeverageConfig `json:"leverage"` // 杠杆配置 + Log *LogConfig `json:"log"` // 日志配置(可选) } // LoadConfig 从文件加载配置 diff --git a/config/database.go b/config/database.go index 932982b4..3749df4d 100644 --- a/config/database.go +++ b/config/database.go @@ -40,6 +40,7 @@ type DatabaseInterface interface { GetTraders(userID string) ([]*TraderRecord, error) UpdateTraderStatus(userID, id string, isRunning bool) error UpdateTrader(trader *TraderRecord) error + UpdateTraderInitialBalance(userID, id string, newBalance float64) error UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error DeleteTrader(userID, id string) error GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) @@ -899,6 +900,12 @@ func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt stri return err } +// UpdateTraderInitialBalance 更新交易员初始余额(用于自动同步交易所实际余额) +func (d *Database) UpdateTraderInitialBalance(userID, id string, newBalance float64) error { + _, err := d.db.Exec(`UPDATE traders SET initial_balance = ? WHERE id = ? AND user_id = ?`, newBalance, id, userID) + return err +} + // DeleteTrader 删除交易员 func (d *Database) DeleteTrader(userID, id string) error { _, err := d.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID) @@ -928,8 +935,13 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM `, traderID, userID).Scan( &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.CreatedAt, &trader.UpdatedAt, &aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey, + &aiModel.CustomAPIURL, &aiModel.CustomModelName, &aiModel.CreatedAt, &aiModel.UpdatedAt, &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, @@ -1065,7 +1077,7 @@ func (d *Database) LoadBetaCodesFromFile(filePath string) error { log.Printf("插入内测码 %s 失败: %v", code, err) continue } - + if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 { insertedCount++ } diff --git a/config/database_pg.go b/config/database_pg.go index b8dd560f..ad11ca07 100644 --- a/config/database_pg.go +++ b/config/database_pg.go @@ -464,6 +464,12 @@ func (d *PostgreSQLDatabase) UpdateTraderCustomPrompt(userID, id string, customP return err } +// UpdateTraderInitialBalance 更新交易员初始余额(用于自动同步交易所实际余额) +func (d *PostgreSQLDatabase) UpdateTraderInitialBalance(userID, id string, newBalance float64) error { + _, err := d.db.Exec(`UPDATE traders SET initial_balance = $1, updated_at = CURRENT_TIMESTAMP WHERE id = $2 AND user_id = $3`, newBalance, id, userID) + return err +} + // DeleteTrader 删除交易员 func (d *PostgreSQLDatabase) DeleteTrader(userID, id string) error { _, err := d.db.Exec(`DELETE FROM traders WHERE id = $1 AND user_id = $2`, id, userID) diff --git a/config/database_test.go b/config/database_test.go deleted file mode 100644 index 45d1ddb4..00000000 --- a/config/database_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package config - -import "testing" - -func TestExample(t *testing.T) { - if 1+1 != 2 { - t.Error("Math is broken") - } -} diff --git a/decision/engine.go b/decision/engine.go index df48d534..598658d1 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -7,10 +7,22 @@ import ( "nofx/market" "nofx/mcp" "nofx/pool" + "regexp" "strings" "time" ) +// 预编译正则表达式(性能优化:避免每次调用时重新编译) +var ( + // ✅ 安全的正則:精確匹配 ```json 代碼塊 + // 使用反引號 + 拼接避免轉義問題 + reJSONFence = regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```") + reJSONArray = regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) + reArrayHead = regexp.MustCompile(`^\[\s*\{`) + reArrayOpenSpace = regexp.MustCompile(`^\[\s+\{`) + reInvisibleRunes = regexp.MustCompile("[\u200B\u200C\u200D\uFEFF]") +) + // PositionInfo 持仓信息 type PositionInfo struct { Symbol string `json:"symbol"` @@ -71,11 +83,20 @@ type Context struct { // Decision AI的交易决策 type Decision struct { Symbol string `json:"symbol"` - Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "hold", "wait" + Action string `json:"action"` // "open_long", "open_short", "close_long", "close_short", "update_stop_loss", "update_take_profit", "partial_close", "hold", "wait" + + // 开仓参数 Leverage int `json:"leverage,omitempty"` PositionSizeUSD float64 `json:"position_size_usd,omitempty"` StopLoss float64 `json:"stop_loss,omitempty"` TakeProfit float64 `json:"take_profit,omitempty"` + + // 调整参数(新增) + NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 update_stop_loss + NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 update_take_profit + ClosePercentage float64 `json:"close_percentage,omitempty"` // 用于 partial_close (0-100) + + // 通用参数 Confidence int `json:"confidence,omitempty"` // 信心度 (0-100) RiskUSD float64 `json:"risk_usd,omitempty"` // 最大美元风险 Reasoning string `json:"reasoning"` @@ -160,17 +181,20 @@ func fetchMarketDataForContext(ctx *Context) error { continue } - // ⚠️ 流动性过滤:持仓价值低于15M USD的币种不做(多空都不做) + // ⚠️ 流动性过滤:持仓价值低于阈值的币种不做(多空都不做) // 持仓价值 = 持仓量 × 当前价格 // 但现有持仓必须保留(需要决策是否平仓) + // 💡 OI 門檻配置:用戶可根據風險偏好調整 + const minOIThresholdMillions = 15.0 // 可調整:15M(保守) / 10M(平衡) / 8M(寬鬆) / 5M(激進) + isExistingPosition := positionSymbols[symbol] if !isExistingPosition && data.OpenInterest != nil && data.CurrentPrice > 0 { // 计算持仓价值(USD)= 持仓量 × 当前价格 oiValue := data.OpenInterest.Latest * data.CurrentPrice oiValueInMillions := oiValue / 1_000_000 // 转换为百万美元单位 - if oiValueInMillions < 15 { - log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < 15M),跳过此币种 [持仓量:%.0f × 价格:%.4f]", - symbol, oiValueInMillions, data.OpenInterest.Latest, data.CurrentPrice) + if oiValueInMillions < minOIThresholdMillions { + log.Printf("⚠️ %s 持仓价值过低(%.2fM USD < %.1fM),跳过此币种 [持仓量:%.0f × 价格:%.4f]", + symbol, oiValueInMillions, minOIThresholdMillions, data.OpenInterest.Latest, data.CurrentPrice) continue } } @@ -200,10 +224,31 @@ func fetchMarketDataForContext(ctx *Context) error { // calculateMaxCandidates 根据账户状态计算需要分析的候选币种数量 func calculateMaxCandidates(ctx *Context) int { - // 直接返回候选池的全部币种数量 - // 因为候选池已经在 auto_trader.go 中筛选过了 - // 固定分析前20个评分最高的币种(来自AI500) - return len(ctx.CandidateCoins) + // ⚠️ 重要:限制候选币种数量,避免 Prompt 过大 + // 根据持仓数量动态调整:持仓越少,可以分析更多候选币 + const ( + maxCandidatesWhenEmpty = 30 // 无持仓时最多分析30个候选币 + maxCandidatesWhenHolding1 = 25 // 持仓1个时最多分析25个候选币 + maxCandidatesWhenHolding2 = 20 // 持仓2个时最多分析20个候选币 + maxCandidatesWhenHolding3 = 15 // 持仓3个时最多分析15个候选币(避免 Prompt 过大) + ) + + positionCount := len(ctx.Positions) + var maxCandidates int + + switch positionCount { + case 0: + maxCandidates = maxCandidatesWhenEmpty + case 1: + maxCandidates = maxCandidatesWhenHolding1 + case 2: + maxCandidates = maxCandidatesWhenHolding2 + default: // 3+ 持仓 + maxCandidates = maxCandidatesWhenHolding3 + } + + // 返回实际候选币数量和上限中的较小值 + return min(len(ctx.CandidateCoins), maxCandidates) } // buildSystemPromptWithCustom 构建包含自定义内容的 System Prompt @@ -264,9 +309,11 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in sb.WriteString("# 硬约束(风险控制)\n\n") sb.WriteString("1. 风险回报比: 必须 ≥ 1:3(冒1%风险,赚3%+收益)\n") sb.WriteString("2. 最多持仓: 3个币种(质量>数量)\n") - sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U(%dx杠杆) | BTC/ETH %.0f-%.0f U(%dx杠杆)\n", - accountEquity*0.8, accountEquity*1.5, altcoinLeverage, accountEquity*5, accountEquity*10, btcEthLeverage)) - sb.WriteString("4. 保证金: 总使用率 ≤ 90%\n\n") + sb.WriteString(fmt.Sprintf("3. 单币仓位: 山寨%.0f-%.0f U | BTC/ETH %.0f-%.0f U\n", + accountEquity*0.8, accountEquity*1.5, accountEquity*5, accountEquity*10)) + sb.WriteString(fmt.Sprintf("4. 杠杆限制: **山寨币最大%dx杠杆** | **BTC/ETH最大%dx杠杆** (⚠️ 严格执行,不可超过)\n", altcoinLeverage, btcEthLeverage)) + sb.WriteString("5. 保证金: 总使用率 ≤ 90%\n") + sb.WriteString("6. 开仓金额: 建议 **≥12 USDT** (交易所最小名义价值 10 USDT + 安全边际)\n\n") // 3. 输出格式 - 动态生成 sb.WriteString("#输出格式\n\n") @@ -430,25 +477,44 @@ func extractCoTTrace(response string) string { // extractDecisions 提取JSON决策列表 func extractDecisions(response string) ([]Decision, error) { - // 直接查找JSON数组 - 找第一个完整的JSON数组 - arrayStart := strings.Index(response, "[") - if arrayStart == -1 { - return nil, fmt.Errorf("无法找到JSON数组起始") + // 预清洗:去零宽/BOM + s := removeInvisibleRunes(response) + s = strings.TrimSpace(s) + + // 🔧 關鍵修復:在正則匹配之前就先修復全角字符! + // 否則正則表達式 \[ 無法匹配全角的 [ + s = fixMissingQuotes(s) + + // 1) 优先从 ```json 代码块中提取 + if m := reJSONFence.FindStringSubmatch(s); m != nil && len(m) > 1 { + jsonContent := strings.TrimSpace(m[1]) + jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" + jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有全角) + if err := validateJSONFormat(jsonContent); err != nil { + return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) + } + var decisions []Decision + if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { + return nil, fmt.Errorf("JSON解析失败: %w\nJSON内容: %s", err, jsonContent) + } + return decisions, nil } - // 从 [ 开始,匹配括号找到对应的 ] - arrayEnd := findMatchingBracket(response, arrayStart) - if arrayEnd == -1 { - return nil, fmt.Errorf("无法找到JSON数组结束") + // 2) 退而求其次:全文寻找首个对象数组 + // 注意:此時 s 已經過 fixMissingQuotes(),全角字符已轉換為半角 + jsonContent := strings.TrimSpace(reJSONArray.FindString(s)) + if jsonContent == "" { + return nil, fmt.Errorf("无法找到JSON数组起始(已嘗試修復全角字符)\n原始響應前200字符: %s", s[:min(200, len(s))]) } - jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1]) + // 🔧 規整格式(此時全角字符已在前面修復過) + jsonContent = compactArrayOpen(jsonContent) + jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有殘留全角) - // 🔧 修复常见的JSON格式错误:缺少引号的字段值 - // 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号) - // 修复为: "reasoning": "内容"} - // 使用简单的字符串扫描而不是正则表达式 - jsonContent = fixMissingQuotes(jsonContent) + // 🔧 验证 JSON 格式(检测常见错误) + if err := validateJSONFormat(jsonContent); err != nil { + return nil, fmt.Errorf("JSON格式验证失败: %w\nJSON内容: %s\n完整响应:\n%s", err, jsonContent, response) + } // 解析JSON var decisions []Decision @@ -459,15 +525,86 @@ func extractDecisions(response string) ([]Decision, error) { return decisions, nil } -// fixMissingQuotes 替换中文引号为英文引号(避免输入法自动转换) +// fixMissingQuotes 替换中文引号和全角字符为英文引号和半角字符(避免AI输出全角JSON字符导致解析失败) func fixMissingQuotes(jsonStr string) string { + // 替换中文引号 jsonStr = strings.ReplaceAll(jsonStr, "\u201c", "\"") // " jsonStr = strings.ReplaceAll(jsonStr, "\u201d", "\"") // " jsonStr = strings.ReplaceAll(jsonStr, "\u2018", "'") // ' jsonStr = strings.ReplaceAll(jsonStr, "\u2019", "'") // ' + + // ⚠️ 替换全角括号、冒号、逗号(防止AI输出全角JSON字符) + jsonStr = strings.ReplaceAll(jsonStr, "[", "[") // U+FF3B 全角左方括号 + jsonStr = strings.ReplaceAll(jsonStr, "]", "]") // U+FF3D 全角右方括号 + jsonStr = strings.ReplaceAll(jsonStr, "{", "{") // U+FF5B 全角左花括号 + jsonStr = strings.ReplaceAll(jsonStr, "}", "}") // U+FF5D 全角右花括号 + jsonStr = strings.ReplaceAll(jsonStr, ":", ":") // U+FF1A 全角冒号 + jsonStr = strings.ReplaceAll(jsonStr, ",", ",") // U+FF0C 全角逗号 + + // ⚠️ 替换CJK标点符号(AI在中文上下文中也可能输出这些) + jsonStr = strings.ReplaceAll(jsonStr, "【", "[") // CJK左方头括号 U+3010 + jsonStr = strings.ReplaceAll(jsonStr, "】", "]") // CJK右方头括号 U+3011 + jsonStr = strings.ReplaceAll(jsonStr, "〔", "[") // CJK左龟壳括号 U+3014 + jsonStr = strings.ReplaceAll(jsonStr, "〕", "]") // CJK右龟壳括号 U+3015 + jsonStr = strings.ReplaceAll(jsonStr, "、", ",") // CJK顿号 U+3001 + + // ⚠️ 替换全角空格为半角空格(JSON中不应该有全角空格) + jsonStr = strings.ReplaceAll(jsonStr, " ", " ") // U+3000 全角空格 + return jsonStr } +// validateJSONFormat 验证 JSON 格式,检测常见错误 +func validateJSONFormat(jsonStr string) error { + trimmed := strings.TrimSpace(jsonStr) + + // 允许 [ 和 { 之间存在任意空白(含零宽) + if !reArrayHead.MatchString(trimmed) { + // 检查是否是纯数字/范围数组(常见错误) + if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") { + return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))]) + } + return fmt.Errorf("JSON 必须以 [{ 开头(允许空白),实际: %s", trimmed[:min(20, len(trimmed))]) + } + + // 检查是否包含范围符号 ~(LLM 常见错误) + if strings.Contains(jsonStr, "~") { + return fmt.Errorf("JSON 中不可包含范围符号 ~,所有数字必须是精确的单一值") + } + + // 检查是否包含千位分隔符(如 98,000) + // 使用简单的模式匹配:数字+逗号+3位数字 + for i := 0; i < len(jsonStr)-4; i++ { + if jsonStr[i] >= '0' && jsonStr[i] <= '9' && + jsonStr[i+1] == ',' && + jsonStr[i+2] >= '0' && jsonStr[i+2] <= '9' && + jsonStr[i+3] >= '0' && jsonStr[i+3] <= '9' && + jsonStr[i+4] >= '0' && jsonStr[i+4] <= '9' { + return fmt.Errorf("JSON 数字不可包含千位分隔符逗号,发现: %s", jsonStr[i:min(i+10, len(jsonStr))]) + } + } + + return nil +} + +// min 返回两个整数中的较小值 +func min(a, b int) int { + if a < b { + return a + } + return b +} + +// removeInvisibleRunes 去除零宽字符和 BOM,避免肉眼看不见的前缀破坏校验 +func removeInvisibleRunes(s string) string { + return reInvisibleRunes.ReplaceAllString(s, "") +} + +// compactArrayOpen 规整开头的 "[ {" → "[{" +func compactArrayOpen(s string) string { + return reArrayOpenSpace.ReplaceAllString(strings.TrimSpace(s), "[{") +} + // validateDecisions 验证所有决策(需要账户信息和杠杆配置) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { @@ -504,12 +641,15 @@ func findMatchingBracket(s string, start int) int { func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { // 验证action validActions := map[string]bool{ - "open_long": true, - "open_short": true, - "close_long": true, - "close_short": true, - "hold": true, - "wait": true, + "open_long": true, + "open_short": true, + "close_long": true, + "close_short": true, + "update_stop_loss": true, + "update_take_profit": true, + "partial_close": true, + "hold": true, + "wait": true, } if !validActions[d.Action] { @@ -532,6 +672,22 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi if d.PositionSizeUSD <= 0 { return fmt.Errorf("仓位大小必须大于0: %.2f", d.PositionSizeUSD) } + + // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) + // Binance 最小名义价值 10 USDT + 安全边际 + const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 + const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) + + if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { + if d.PositionSizeUSD < minPositionSizeBTCETH { + return fmt.Errorf("%s 开仓金额过小(%.2f USDT),必须≥%.2f USDT(因价格高且精度限制,避免数量四舍五入为0)", d.Symbol, d.PositionSizeUSD, minPositionSizeBTCETH) + } + } else { + if d.PositionSizeUSD < minPositionSizeGeneral { + return fmt.Errorf("开仓金额过小(%.2f USDT),必须≥%.2f USDT(Binance 最小名义价值要求)", d.PositionSizeUSD, minPositionSizeGeneral) + } + } + // 验证仓位价值上限(加1%容差以避免浮点数精度问题) tolerance := maxPositionValue * 0.01 // 1%容差 if d.PositionSizeUSD > maxPositionValue+tolerance { @@ -589,5 +745,26 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi } } + // 动态调整止损验证 + if d.Action == "update_stop_loss" { + if d.NewStopLoss <= 0 { + return fmt.Errorf("新止损价格必须大于0: %.2f", d.NewStopLoss) + } + } + + // 动态调整止盈验证 + if d.Action == "update_take_profit" { + if d.NewTakeProfit <= 0 { + return fmt.Errorf("新止盈价格必须大于0: %.2f", d.NewTakeProfit) + } + } + + // 部分平仓验证 + if d.Action == "partial_close" { + if d.ClosePercentage <= 0 || d.ClosePercentage > 100 { + return fmt.Errorf("平仓百分比必须在0-100之间: %.1f", d.ClosePercentage) + } + } + return nil } diff --git a/docker-compose.yml b/docker-compose.yml index 6a60bf54..31d5a474 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -56,6 +56,7 @@ services: - /etc/localtime:/etc/localtime:ro # Sync host time environment: - TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone + - AI_MAX_TOKENS=4000 # AI响应的最大token数(默认2000,建议4000-8000) - POSTGRES_HOST=postgres - POSTGRES_PORT=5432 - POSTGRES_DB=${POSTGRES_DB:-nofx} diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend index 1145d501..7bd02348 100644 --- a/docker/Dockerfile.backend +++ b/docker/Dockerfile.backend @@ -47,7 +47,9 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=1 GOOS=linux CGO_CFLAGS="-D_LARGEFILE64_SOURCE" 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) diff --git a/docs/community/bounty-guide.md b/docs/community/bounty-guide.md index a6b841d2..326ab726 100644 --- a/docs/community/bounty-guide.md +++ b/docs/community/bounty-guide.md @@ -197,7 +197,7 @@ Details: [详情链接] ### 法律 & 合规 - ✅ 明确说明这是开源贡献,不是雇佣关系 -- ✅ 确保贡献者同意 MIT License +- ✅ 确保贡献者同意 AGPL-3.0 License - ✅ 保留最终合并决定权 ### 资金管理 diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index c35926f9..9e1740f7 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -23,7 +23,7 @@ Choose the method that best fits your needs: **Quick Start:** ```bash -cp config.example.jsonc config.json +cp config.json.example config.json ./start.sh start --build ``` diff --git a/docs/getting-started/README.zh-CN.md b/docs/getting-started/README.zh-CN.md index aafe5cfb..836619f6 100644 --- a/docs/getting-started/README.zh-CN.md +++ b/docs/getting-started/README.zh-CN.md @@ -21,7 +21,7 @@ **快速开始:** ```bash -cp config.example.jsonc config.json +cp config.json.example config.json ./start.sh start --build ``` diff --git a/docs/getting-started/docker-deploy.en.md b/docs/getting-started/docker-deploy.en.md index bd909273..7cb4e3d2 100644 --- a/docs/getting-started/docker-deploy.en.md +++ b/docs/getting-started/docker-deploy.en.md @@ -50,7 +50,7 @@ docker compose --version # Docker 24+ includes this, no separate installation n ```bash # Copy configuration template -cp config.example.jsonc config.json +cp config.json.example config.json # Edit configuration file with your API keys nano config.json # or use any other editor @@ -267,7 +267,7 @@ kill -9 # ~~ls -la config.json~~ # ~~If not exists, copy template~~ -# ~~cp config.example.jsonc config.json~~ +# ~~cp config.json.example config.json~~ *Note: Now using SQLite database for configuration storage, no longer need config.json* ``` diff --git a/docs/getting-started/docker-deploy.zh-CN.md b/docs/getting-started/docker-deploy.zh-CN.md index 49667356..0840ea50 100644 --- a/docs/getting-started/docker-deploy.zh-CN.md +++ b/docs/getting-started/docker-deploy.zh-CN.md @@ -55,7 +55,7 @@ docker compose --version # Docker 24+ 自带,无需单独安装 ```bash # 复制配置文件模板 -cp config.example.jsonc config.json +cp config.json.example config.json # 编辑配置文件,填入你的 API 密钥 nano config.json # 或使用其他编辑器 @@ -270,7 +270,7 @@ kill -9 ls -la config.json # 如果不存在,复制模板 -cp config.example.jsonc config.json +cp config.json.example config.json ``` ### 健康检查失败 diff --git a/docs/i18n/ru/README.md b/docs/i18n/ru/README.md index bcc79622..816e0823 100644 --- a/docs/i18n/ru/README.md +++ b/docs/i18n/ru/README.md @@ -3,7 +3,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) [![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) -[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE) [![Backed by Amber.ac](https://img.shields.io/badge/Backed%20by-Amber.ac-orange.svg)](https://amber.ac) **Языки / Languages:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Українська](../uk/README.md) | [Русский](../ru/README.md) @@ -285,7 +285,7 @@ Docker автоматически обрабатывает все зависим #### Шаг 1: Подготовьте конфигурацию ```bash # Скопируйте шаблон конфигурации -cp config.example.jsonc config.json +cp config.json.example config.json # Отредактируйте и заполните ваши API ключи nano config.json # или используйте любой редактор @@ -320,6 +320,7 @@ docker compose up -d --build - **Русский**: См. документацию Docker (скоро будет доступно) - **English**: See [DOCKER_DEPLOY.en.md](DOCKER_DEPLOY.en.md) - **中文**: 查看 [DOCKER_DEPLOY.md](DOCKER_DEPLOY.md) +- **日本語**: [DOCKER_DEPLOY.ja.md](DOCKER_DEPLOY.ja.md)を参照 --- @@ -423,7 +424,7 @@ cd .. **Шаг 1**: Скопируйте и переименуйте файл примера конфигурации ```bash -cp config.example.jsonc config.json +cp config.json.example config.json ``` **Шаг 2**: Отредактируйте `config.json` и заполните ваши API ключи diff --git a/docs/i18n/uk/README.md b/docs/i18n/uk/README.md index 78bddc72..300ca470 100644 --- a/docs/i18n/uk/README.md +++ b/docs/i18n/uk/README.md @@ -3,7 +3,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) [![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) -[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE) [![Backed by Amber.ac](https://img.shields.io/badge/Backed%20by-Amber.ac-orange.svg)](https://amber.ac) **Мови / Languages:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Українська](../uk/README.md) | [Русский](../ru/README.md) @@ -288,7 +288,7 @@ Docker автоматично обробляє всі залежності (Go, #### Крок 1: Підготуйте конфігурацію ```bash # Скопіюйте шаблон конфігурації -cp config.example.jsonc config.json +cp config.json.example config.json # Відредагуйте та заповніть ваші API ключі nano config.json # або використайте будь-який редактор @@ -323,6 +323,7 @@ docker compose up -d --build - **Українська**: Дивіться документацію Docker (скоро буде доступно) - **English**: See [DOCKER_DEPLOY.en.md](DOCKER_DEPLOY.en.md) - **中文**: 查看 [DOCKER_DEPLOY.md](DOCKER_DEPLOY.md) +- **日本語**: [DOCKER_DEPLOY.ja.md](DOCKER_DEPLOY.ja.md)を参照 --- @@ -426,7 +427,7 @@ cd .. **Крок 1**: Скопіюйте та перейменуйте файл прикладу конфігурації ```bash -cp config.example.jsonc config.json +cp config.json.example config.json ``` **Крок 2**: Відредагуйте `config.json` та заповніть ваші API ключі diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 5bfd283c..d61236fe 100644 --- a/docs/i18n/zh-CN/README.md +++ b/docs/i18n/zh-CN/README.md @@ -3,7 +3,7 @@ [![Go Version](https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go)](https://golang.org/) [![React](https://img.shields.io/badge/React-18+-61DAFB?style=flat&logo=react)](https://reactjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?style=flat&logo=typescript)](https://www.typescriptlang.org/) -[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![License](https://img.shields.io/badge/License-AGPL--3.0-blue.svg)](LICENSE) [![Backed by Amber.ac](https://img.shields.io/badge/Backed%20by-Amber.ac-orange.svg)](https://amber.ac) **语言 / Languages:** [English](../../../README.md) | [中文](../zh-CN/README.md) | [Українська](../uk/README.md) | [Русский](../ru/README.md) @@ -283,7 +283,7 @@ Docker会自动处理所有依赖(Go、Node.js、TA-Lib)和环境配置, #### 步骤1:准备配置文件 ```bash # 复制配置文件模板 -cp config.example.jsonc config.json +cp config.json.example config.json # 编辑并填入你的API密钥 nano config.json # 或使用其他编辑器 @@ -319,6 +319,7 @@ docker compose up -d --build **📖 详细的Docker部署教程、故障排查和高级配置:** - **中文**: 查看 [DOCKER_DEPLOY.md](DOCKER_DEPLOY.md) - **English**: See [DOCKER_DEPLOY.en.md](DOCKER_DEPLOY.en.md) +- **日本語**: [DOCKER_DEPLOY.ja.md](DOCKER_DEPLOY.ja.md)を参照 --- @@ -422,7 +423,7 @@ cd .. ~~**步骤1**:复制并重命名示例配置文件~~ ```bash -cp config.example.jsonc config.json +cp config.json.example config.json ``` ~~**步骤2**:编辑`config.json`填入您的API密钥~~ @@ -1262,7 +1263,15 @@ sudo apt-get install libta-lib0-dev ## 📄 开源协议 -MIT License - 详见 [LICENSE](LICENSE) 文件 +本项目采用 **GNU Affero 通用公共许可证 v3.0 (AGPL-3.0)** - 详见 [LICENSE](LICENSE) 文件 + +**这意味着什么:** +- ✅ 你可以使用、修改和分发此软件 +- ✅ 你必须公开你修改版本的源代码 +- ✅ 如果你在服务器上运行修改版本,必须向用户提供源代码 +- ✅ 所有衍生作品也必须使用 AGPL-3.0 许可证 + +如需商业许可或有疑问,请联系维护者。 --- diff --git a/go.mod b/go.mod index a9dcea75..9b0f63e9 100644 --- a/go.mod +++ b/go.mod @@ -6,14 +6,17 @@ require ( github.com/adshao/go-binance/v2 v2.8.7 github.com/ethereum/go-ethereum v1.16.5 github.com/gin-gonic/gin v1.11.0 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.32 github.com/pquerna/otp v1.4.0 + github.com/sirupsen/logrus v1.9.3 github.com/sonirico/go-hyperliquid v0.17.0 golang.org/x/crypto v0.42.0 + modernc.org/sqlite v1.40.0 ) require ( @@ -28,6 +31,7 @@ require ( 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/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-windows v1.0.2 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect @@ -51,11 +55,13 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect github.com/quic-go/qpack v0.5.1 // 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/shopspring/decimal v1.4.0 // indirect github.com/sonirico/vago v0.9.0 // indirect @@ -71,6 +77,7 @@ require ( go.elastic.co/fastjson v1.5.1 // indirect go.uber.org/mock v0.5.0 // indirect golang.org/x/arch v0.20.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect @@ -79,4 +86,7 @@ require ( golang.org/x/tools v0.36.0 // indirect google.golang.org/protobuf v1.36.9 // indirect howett.net/plist v1.0.1 // indirect + modernc.org/libc v1.66.10 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 18fb8d77..2494cec0 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ 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/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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +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/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZPk3G244lHrU= github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= @@ -62,6 +64,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -80,6 +84,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -130,6 +136,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OH github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -146,6 +154,8 @@ 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/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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -157,6 +167,8 @@ github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1 github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sonirico/go-hyperliquid v0.17.0 h1:eXYACWupwu41O1VtKw17dqe9oOLQ1A2nRElGhg5Ox+4= github.com/sonirico/go-hyperliquid v0.17.0/go.mod h1:sH51Vsu+tPUwc95TL2MoQ8YXSewLWBEJirgzo7sZx6w= github.com/sonirico/vago v0.9.0 h1:DF2OWW2Aaf1xPZmnFv79kBrHmjKX3mVvMbP08vERlKo= @@ -169,6 +181,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -202,12 +215,15 @@ golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -230,3 +246,29 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.40.0 h1:bNWEDlYhNPAUdUdBzjAvn8icAs/2gaKlj4vM+tQ6KdQ= +modernc.org/sqlite v1.40.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/logger/config.go b/logger/config.go new file mode 100644 index 00000000..32774558 --- /dev/null +++ b/logger/config.go @@ -0,0 +1,64 @@ +package logger + +import ( + "github.com/sirupsen/logrus" +) + +// Config 日志配置(简化版) +type Config struct { + Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info) + Telegram *TelegramConfig `json:"telegram"` // Telegram推送配置(可选) +} + +// TelegramConfig Telegram推送配置(简化版,高级参数使用默认值) +type TelegramConfig struct { + Enabled bool `json:"enabled"` // 是否启用(默认: false) + BotToken string `json:"bot_token"` // Bot Token + ChatID int64 `json:"chat_id"` // Chat ID + MinLevel string `json:"min_level"` // 最低日志级别,该级别及以上的日志会推送到Telegram(可选,默认: error) +} + +// SetDefaults 设置默认值 +func (c *Config) SetDefaults() { + if c.Level == "" { + c.Level = "info" + } +} + +// GetLogrusLevels 返回要推送到Telegram的日志级别 +// 根据配置的MinLevel返回该级别及以上的所有日志级别 +// 如果未配置或配置无效,默认返回error, fatal, panic(向后兼容) +func (tc *TelegramConfig) GetLogrusLevels() []logrus.Level { + // 如果未配置,使用默认值error(向后兼容) + minLevelStr := tc.MinLevel + if minLevelStr == "" { + minLevelStr = "error" + } + + // 解析配置的日志级别 + minLevel, err := logrus.ParseLevel(minLevelStr) + if err != nil { + // 如果解析失败,使用默认值error(向后兼容) + minLevel = logrus.ErrorLevel + } + + // 定义所有日志级别(从高到低:panic, fatal, error, warn, info, debug) + allLevels := []logrus.Level{ + logrus.PanicLevel, + logrus.FatalLevel, + logrus.ErrorLevel, + logrus.WarnLevel, + logrus.InfoLevel, + logrus.DebugLevel, + } + + // 返回所有大于等于minLevel的日志级别 + var result []logrus.Level + for _, level := range allLevels { + if level <= minLevel { + result = append(result, level) + } + } + + return result +} diff --git a/logger/config.telegram.json b/logger/config.telegram.json new file mode 100644 index 00000000..197c0802 --- /dev/null +++ b/logger/config.telegram.json @@ -0,0 +1,33 @@ +{ + "traders": [ + { + "id": "trader1", + "name": "AI Trader 1", + "enabled": true, + "ai_model": "deepseek", + "exchange": "binance", + "binance_api_key": "your_api_key", + "binance_secret_key": "your_secret_key", + "deepseek_key": "your_deepseek_key", + "initial_balance": 1000, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "default_coins": ["BTCUSDT", "ETHUSDT", "SOLUSDT"], + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, + "log": { + "level": "info", + "telegram": { + "enabled": true, + "bot_token": "79472419:feafe231414", + "chat_id": -100323252626, + "min_level": "error" + } + }, + "_comment": "日志配置说明:level 可选值为 debug/info/warn/error,默认 info。telegram 部分作为可选配置, Telegram 推送默认为 error/fatal/panic 级别,min_level 如果设置为warn,则推送warn级别及以上的日志" +} diff --git a/logger/decision_logger.go b/logger/decision_logger.go index efa5ab74..c9630508 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -50,9 +50,9 @@ type PositionSnapshot struct { // DecisionAction 决策动作 type DecisionAction struct { - Action string `json:"action"` // open_long, open_short, close_long, close_short + Action string `json:"action"` // open_long, open_short, close_long, close_short, update_stop_loss, update_take_profit, partial_close Symbol string `json:"symbol"` // 币种 - Quantity float64 `json:"quantity"` // 数量 + Quantity float64 `json:"quantity"` // 数量(部分平仓时使用) Leverage int `json:"leverage"` // 杠杆(开仓时) Price float64 `json:"price"` // 执行价格 OrderID int64 `json:"order_id"` // 订单ID @@ -243,8 +243,11 @@ func (l *DecisionLogger) GetStatistics() (*Statistics, error) { switch action.Action { case "open_long", "open_short": stats.TotalOpenPositions++ - case "close_long", "close_short": + case "close_long", "close_short", "auto_close_long", "auto_close_short": stats.TotalClosePositions++ + // 🔧 BUG FIX:partial_close 不計入 TotalClosePositions,避免重複計數 + // case "partial_close": // 不計數,因為只有完全平倉才算一次 + // update_stop_loss 和 update_take_profit 不計入統計 } } } @@ -348,11 +351,22 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna symbol := action.Symbol side := "" - if action.Action == "open_long" || action.Action == "close_long" { + if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" || action.Action == "auto_close_long" { side = "long" - } else if action.Action == "open_short" || action.Action == "close_short" { + } else if action.Action == "open_short" || action.Action == "close_short" || action.Action == "auto_close_short" { side = "short" } + + // partial_close 需要根據持倉判斷方向 + if action.Action == "partial_close" && side == "" { + for key, pos := range openPositions { + if posSymbol, _ := pos["side"].(string); key == symbol+"_"+posSymbol { + side = posSymbol + break + } + } + } + posKey := symbol + "_" + side switch action.Action { @@ -365,9 +379,10 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna "quantity": action.Quantity, "leverage": action.Leverage, } - case "close_long", "close_short": + case "close_long", "close_short", "auto_close_long", "auto_close_short": // 移除已平仓记录 delete(openPositions, posKey) + // partial_close 不處理,保留持倉記錄 } } } @@ -382,25 +397,41 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna symbol := action.Symbol side := "" - if action.Action == "open_long" || action.Action == "close_long" { + if action.Action == "open_long" || action.Action == "close_long" || action.Action == "partial_close" || action.Action == "auto_close_long" { side = "long" - } else if action.Action == "open_short" || action.Action == "close_short" { + } else if action.Action == "open_short" || action.Action == "close_short" || action.Action == "auto_close_short" { side = "short" } + + // partial_close 需要根據持倉判斷方向 + if action.Action == "partial_close" { + // 從 openPositions 中查找持倉方向 + for key, pos := range openPositions { + if posSymbol, _ := pos["side"].(string); key == symbol+"_"+posSymbol { + side = posSymbol + break + } + } + } + posKey := symbol + "_" + side // 使用symbol_side作为key,区分多空持仓 switch action.Action { case "open_long", "open_short": // 更新开仓记录(可能已经在预填充时记录过了) openPositions[posKey] = map[string]interface{}{ - "side": side, - "openPrice": action.Price, - "openTime": action.Timestamp, - "quantity": action.Quantity, - "leverage": action.Leverage, + "side": side, + "openPrice": action.Price, + "openTime": action.Timestamp, + "quantity": action.Quantity, + "leverage": action.Leverage, + "remainingQuantity": action.Quantity, // 🔧 BUG FIX:追蹤剩餘數量 + "accumulatedPnL": 0.0, // 🔧 BUG FIX:累積部分平倉盈虧 + "partialCloseCount": 0, // 🔧 BUG FIX:部分平倉次數 + "partialCloseVolume": 0.0, // 🔧 BUG FIX:部分平倉總量 } - case "close_long", "close_short": + case "close_long", "close_short", "partial_close", "auto_close_long", "auto_close_short": // 查找对应的开仓记录(可能来自预填充或当前窗口) if openPos, exists := openPositions[posKey]; exists { openPrice := openPos["openPrice"].(float64) @@ -409,71 +440,159 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) - // 计算实际盈亏(USDT) - // 合约交易 PnL 计算:quantity × 价格差 - // 注意:杠杆不影响绝对盈亏,只影响保证金需求 + // 🔧 BUG FIX:取得追蹤字段(若不存在則初始化) + remainingQty, _ := openPos["remainingQuantity"].(float64) + if remainingQty == 0 { + remainingQty = quantity // 兼容舊數據(沒有 remainingQuantity 字段) + } + accumulatedPnL, _ := openPos["accumulatedPnL"].(float64) + partialCloseCount, _ := openPos["partialCloseCount"].(int) + partialCloseVolume, _ := openPos["partialCloseVolume"].(float64) + + // 对于 partial_close,使用实际平仓数量;否则使用剩余仓位数量 + actualQuantity := remainingQty + if action.Action == "partial_close" { + actualQuantity = action.Quantity + } + + // 计算本次平仓的盈亏(USDT) var pnl float64 if side == "long" { - pnl = quantity * (action.Price - openPrice) + pnl = actualQuantity * (action.Price - openPrice) } else { - pnl = quantity * (openPrice - action.Price) + pnl = actualQuantity * (openPrice - action.Price) } - // 计算盈亏百分比(相对保证金) - positionValue := quantity * openPrice - marginUsed := positionValue / float64(leverage) - pnlPct := 0.0 - if marginUsed > 0 { - pnlPct = (pnl / marginUsed) * 100 - } + // 🔧 BUG FIX:處理 partial_close 聚合邏輯 + if action.Action == "partial_close" { + // 累積盈虧和數量 + accumulatedPnL += pnl + remainingQty -= actualQuantity + partialCloseCount++ + partialCloseVolume += actualQuantity - // 记录交易结果 - outcome := TradeOutcome{ - Symbol: symbol, - Side: side, - Quantity: quantity, - Leverage: leverage, - OpenPrice: openPrice, - ClosePrice: action.Price, - PositionValue: positionValue, - MarginUsed: marginUsed, - PnL: pnl, - PnLPct: pnlPct, - Duration: action.Timestamp.Sub(openTime).String(), - OpenTime: openTime, - CloseTime: action.Timestamp, - } + // 更新 openPositions(保留持倉記錄,但更新追蹤數據) + openPos["remainingQuantity"] = remainingQty + openPos["accumulatedPnL"] = accumulatedPnL + openPos["partialCloseCount"] = partialCloseCount + openPos["partialCloseVolume"] = partialCloseVolume - analysis.RecentTrades = append(analysis.RecentTrades, outcome) - analysis.TotalTrades++ + // 判斷是否已完全平倉 + if remainingQty <= 0.0001 { // 使用小閾值避免浮點誤差 + // ✅ 完全平倉:記錄為一筆完整交易 + positionValue := quantity * openPrice + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (accumulatedPnL / marginUsed) * 100 + } - // 分类交易:盈利、亏损、持平(避免将pnl=0算入亏损) - if pnl > 0 { - analysis.WinningTrades++ - analysis.AvgWin += pnl - } else if pnl < 0 { - analysis.LosingTrades++ - analysis.AvgLoss += pnl - } - // pnl == 0 的交易不计入盈利也不计入亏损,但计入总交易数 + outcome := TradeOutcome{ + Symbol: symbol, + Side: side, + Quantity: quantity, // 使用原始總量 + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, // 最後一次平倉價格 + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: accumulatedPnL, // 🔧 使用累積盈虧 + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, + } - // 更新币种统计 - if _, exists := analysis.SymbolStats[symbol]; !exists { - analysis.SymbolStats[symbol] = &SymbolPerformance{ - Symbol: symbol, + analysis.RecentTrades = append(analysis.RecentTrades, outcome) + analysis.TotalTrades++ // 🔧 只在完全平倉時計數 + + // 分类交易 + if accumulatedPnL > 0 { + analysis.WinningTrades++ + analysis.AvgWin += accumulatedPnL + } else if accumulatedPnL < 0 { + analysis.LosingTrades++ + analysis.AvgLoss += accumulatedPnL + } + + // 更新币种统计 + if _, exists := analysis.SymbolStats[symbol]; !exists { + analysis.SymbolStats[symbol] = &SymbolPerformance{ + Symbol: symbol, + } + } + stats := analysis.SymbolStats[symbol] + stats.TotalTrades++ + stats.TotalPnL += accumulatedPnL + if accumulatedPnL > 0 { + stats.WinningTrades++ + } else if accumulatedPnL < 0 { + stats.LosingTrades++ + } + + // 刪除持倉記錄 + delete(openPositions, posKey) } - } - stats := analysis.SymbolStats[symbol] - stats.TotalTrades++ - stats.TotalPnL += pnl - if pnl > 0 { - stats.WinningTrades++ - } else if pnl < 0 { - stats.LosingTrades++ - } + // ⚠️ 否則不做任何操作(等待後續 partial_close 或 full close) - // 移除已平仓记录 - delete(openPositions, posKey) + } else { + // 🔧 完全平倉(close_long/close_short/auto_close) + // 如果之前有部分平倉,需要加上累積的 PnL + totalPnL := accumulatedPnL + pnl + + positionValue := quantity * openPrice + marginUsed := positionValue / float64(leverage) + pnlPct := 0.0 + if marginUsed > 0 { + pnlPct = (totalPnL / marginUsed) * 100 + } + + outcome := TradeOutcome{ + Symbol: symbol, + Side: side, + Quantity: quantity, // 使用原始總量 + Leverage: leverage, + OpenPrice: openPrice, + ClosePrice: action.Price, + PositionValue: positionValue, + MarginUsed: marginUsed, + PnL: totalPnL, // 🔧 包含之前部分平倉的 PnL + PnLPct: pnlPct, + Duration: action.Timestamp.Sub(openTime).String(), + OpenTime: openTime, + CloseTime: action.Timestamp, + } + + analysis.RecentTrades = append(analysis.RecentTrades, outcome) + analysis.TotalTrades++ + + // 分类交易 + if totalPnL > 0 { + analysis.WinningTrades++ + analysis.AvgWin += totalPnL + } else if totalPnL < 0 { + analysis.LosingTrades++ + analysis.AvgLoss += totalPnL + } + + // 更新币种统计 + if _, exists := analysis.SymbolStats[symbol]; !exists { + analysis.SymbolStats[symbol] = &SymbolPerformance{ + Symbol: symbol, + } + } + stats := analysis.SymbolStats[symbol] + stats.TotalTrades++ + stats.TotalPnL += totalPnL + if totalPnL > 0 { + stats.WinningTrades++ + } else if totalPnL < 0 { + stats.LosingTrades++ + } + + // 刪除持倉記錄 + delete(openPositions, posKey) + } } } } diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 00000000..527c46e2 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,210 @@ +package logger + +import ( + "nofx/config" + "os" + + "github.com/sirupsen/logrus" +) + +var ( + // Log 全局logger实例 + Log *logrus.Logger + + // telegramHook 保存hook引用,用于优雅关闭 + telegramHook *TelegramHook +) + +// ============================================================================ +// 初始化函数 +// ============================================================================ + +// Init 初始化全局logger +// 如果config为nil,使用默认配置(console输出,info级别) +func Init(cfg *Config) error { + Log = logrus.New() + + // 如果没有配置,使用默认值 + if cfg == nil { + cfg = &Config{Level: "info"} + } + + // 设置默认值 + cfg.SetDefaults() + + // 设置日志级别 + level, err := logrus.ParseLevel(cfg.Level) + if err != nil { + level = logrus.InfoLevel + } + Log.SetLevel(level) + + // 设置格式化器(固定使用彩色文本格式) + Log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + ForceColors: true, + }) + + // 设置输出目标(默认stdout) + Log.SetOutput(os.Stdout) + + // 启用调用位置信息 + Log.SetReportCaller(true) + + // 添加Telegram Hook(可选) + if cfg.Telegram != nil && cfg.Telegram.Enabled { + if err := setupTelegramHook(cfg.Telegram); err != nil { + Log.Warnf("初始化Telegram推送失败,将继续使用普通日志: %v", err) + } + } + + return nil +} + +// setupTelegramHook 设置Telegram Hook +func setupTelegramHook(telegramCfg *TelegramConfig) error { + hook, err := NewTelegramHook(telegramCfg) + if err != nil { + return err + } + + Log.AddHook(hook) + telegramHook = hook + Log.Info("✅ Telegram日志推送已启用") + return nil +} + +// InitWithSimpleConfig 使用简化配置初始化logger +// 适用于只需要基本功能的场景 +func InitWithSimpleConfig(level string) error { + return Init(&Config{Level: level}) +} + +// InitWithTelegram 使用Telegram配置初始化logger +func InitWithTelegram(botToken string, chatID int64) error { + return Init(&Config{ + Level: "info", + Telegram: &TelegramConfig{ + Enabled: true, + BotToken: botToken, + ChatID: chatID, + }, + }) +} + +// InitFromLogConfig 从config.LogConfig初始化logger +func InitFromLogConfig(logConfig *config.LogConfig) error { + if logConfig == nil { + return InitWithSimpleConfig("info") + } + + cfg := &Config{ + Level: logConfig.Level, + } + + if cfg.Level == "" { + cfg.Level = "info" + } + + // 如果启用了Telegram,添加配置 + if logConfig.Telegram != nil && logConfig.Telegram.Enabled { + if botToken := logConfig.Telegram.BotToken; botToken != "" && logConfig.Telegram.ChatID != 0 { + cfg.Telegram = &TelegramConfig{ + Enabled: true, + BotToken: botToken, + ChatID: logConfig.Telegram.ChatID, + MinLevel: logConfig.Telegram.MinLevel, + } + } + } + + return Init(cfg) +} + +// InitFromParams 从参数初始化logger +// 适用于不依赖config包的场景 +func InitFromParams(level string, telegramEnabled bool, botToken string, chatID int64) error { + cfg := &Config{Level: level} + + if telegramEnabled && botToken != "" && chatID != 0 { + cfg.Telegram = &TelegramConfig{ + Enabled: true, + BotToken: botToken, + ChatID: chatID, + } + } + + return Init(cfg) +} + +// Shutdown 优雅关闭logger(主要用于关闭Telegram发送器) +func Shutdown() { + if telegramHook != nil { + telegramHook.Stop() + telegramHook = nil + } +} + +// ============================================================================ +// 日志记录函数 +// ============================================================================ + +// WithFields 创建带字段的logger entry +func WithFields(fields logrus.Fields) *logrus.Entry { + return Log.WithFields(fields) +} + +// WithField 创建带单个字段的logger entry +func WithField(key string, value interface{}) *logrus.Entry { + return Log.WithField(key, value) +} + +// add debug, info, warn +func Debug(args ...interface{}) { + Log.Debug(args...) +} + +func Info(args ...interface{}) { + Log.Info(args...) +} + +func Warn(args ...interface{}) { + Log.Warn(args...) +} + +func Debugf(format string, args ...interface{}) { + Log.Debugf(format, args...) +} + +func Infof(format string, args ...interface{}) { + Log.Infof(format, args...) +} + +func Warnf(format string, args ...interface{}) { + Log.Warnf(format, args...) +} + +func Error(args ...interface{}) { + Log.Error(args...) +} + +func Errorf(format string, args ...interface{}) { + Log.Errorf(format, args...) +} + +func Fatal(args ...interface{}) { + Log.Fatal(args...) +} + +func Fatalf(format string, args ...interface{}) { + Log.Fatalf(format, args...) +} + +func Panic(args ...interface{}) { + Log.Panic(args...) +} + +func Panicf(format string, args ...interface{}) { + Log.Panicf(format, args...) +} diff --git a/logger/telegram_hook.go b/logger/telegram_hook.go new file mode 100644 index 00000000..e8477f47 --- /dev/null +++ b/logger/telegram_hook.go @@ -0,0 +1,158 @@ +package logger + +import ( + "fmt" + "runtime" + "strings" + + "github.com/sirupsen/logrus" +) + +// TelegramHook 实现logrus.Hook接口,将日志推送到Telegram +type TelegramHook struct { + sender *TelegramSender + levels []logrus.Level + enabled bool +} + +// NewTelegramHook 创建Telegram Hook +func NewTelegramHook(config *TelegramConfig) (*TelegramHook, error) { + if !config.Enabled { + return &TelegramHook{enabled: false}, nil + } + + if config.BotToken == "" || config.ChatID == 0 { + return nil, fmt.Errorf("telegram配置不完整: bot_token和chat_id不能为空") + } + + // 创建发送器(使用默认参数) + sender, err := NewTelegramSender(config.BotToken, config.ChatID) + if err != nil { + return nil, fmt.Errorf("创建telegram发送器失败: %w", err) + } + + hook := &TelegramHook{ + sender: sender, + levels: config.GetLogrusLevels(), + enabled: true, + } + + return hook, nil +} + +// Levels 返回需要触发的日志级别 +func (h *TelegramHook) Levels() []logrus.Level { + if !h.enabled { + return []logrus.Level{} + } + return h.levels +} + +// Fire 当日志触发时调用 +func (h *TelegramHook) Fire(entry *logrus.Entry) error { + if !h.enabled { + return nil + } + + // 格式化消息 + message := h.formatMessage(entry) + + // 异步发送(非阻塞) + h.sender.SendAsync(message) + + return nil +} + +// formatMessage 格式化日志消息为Telegram格式 +func (h *TelegramHook) formatMessage(entry *logrus.Entry) string { + // 级别emoji + levelEmoji := h.getLevelEmoji(entry.Level) + + // 基本信息 + var builder strings.Builder + builder.WriteString(fmt.Sprintf("%s *%s*: 系统日志警报\n", levelEmoji, strings.ToUpper(entry.Level.String()))) + builder.WriteString(fmt.Sprintf("📝 消息: `%s`\n", escapeMarkdown(entry.Message))) + + // 字段信息 + if len(entry.Data) > 0 { + builder.WriteString("📊 字段:\n") + for key, value := range entry.Data { + builder.WriteString(fmt.Sprintf(" • %s: `%v`\n", key, value)) + } + } + + // 调用位置 + if entry.HasCaller() { + file := entry.Caller.File + // 只保留相对路径 + if idx := strings.Index(file, "nofx/"); idx >= 0 { + file = file[idx:] + } + builder.WriteString(fmt.Sprintf("📍 位置: `%s:%d`\n", file, entry.Caller.Line)) + } else { + // 如果entry没有caller,手动获取 + if _, file, line, ok := runtime.Caller(8); ok { + if idx := strings.Index(file, "nofx/"); idx >= 0 { + file = file[idx:] + } + builder.WriteString(fmt.Sprintf("📍 位置: `%s:%d`\n", file, line)) + } + } + + // 时间戳 + builder.WriteString(fmt.Sprintf("🕐 时间: `%s`", entry.Time.Format("2006-01-02 15:04:05"))) + + return builder.String() +} + +// getLevelEmoji 获取日志级别对应的emoji +func (h *TelegramHook) getLevelEmoji(level logrus.Level) string { + switch level { + case logrus.PanicLevel: + return "🔴" + case logrus.FatalLevel: + return "🔴" + case logrus.ErrorLevel: + return "🟠" + case logrus.WarnLevel: + return "🟡" + case logrus.InfoLevel: + return "🟢" + case logrus.DebugLevel: + return "🔵" + default: + return "⚪" + } +} + +// escapeMarkdown 转义Markdown特殊字符 +func escapeMarkdown(text string) string { + replacer := strings.NewReplacer( + "_", "\\_", + "*", "\\*", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "~", "\\~", + "`", "\\`", + ">", "\\>", + "#", "\\#", + "+", "\\+", + "-", "\\-", + "=", "\\=", + "|", "\\|", + "{", "\\{", + "}", "\\}", + ".", "\\.", + "!", "\\!", + ) + return replacer.Replace(text) +} + +// Stop 停止Hook(优雅关闭) +func (h *TelegramHook) Stop() { + if h.enabled && h.sender != nil { + h.sender.Stop() + } +} diff --git a/logger/telegram_sender.go b/logger/telegram_sender.go new file mode 100644 index 00000000..8013dc18 --- /dev/null +++ b/logger/telegram_sender.go @@ -0,0 +1,120 @@ +package logger + +import ( + "fmt" + "sync" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +// TelegramSender Telegram消息发送器(异步) +type TelegramSender struct { + bot *tgbotapi.BotAPI + chatID int64 + msgChan chan string + retryCount int + retryInterval time.Duration + wg sync.WaitGroup + stopChan chan struct{} + once sync.Once +} + +// NewTelegramSender 创建Telegram发送器(使用默认参数) +func NewTelegramSender(botToken string, chatID int64) (*TelegramSender, error) { + bot, err := tgbotapi.NewBotAPI(botToken) + if err != nil { + return nil, fmt.Errorf("创建telegram bot失败: %w", err) + } + + // 设置为静默模式(不打印bot信息) + bot.Debug = false + + sender := &TelegramSender{ + bot: bot, + chatID: chatID, + msgChan: make(chan string, 20), // 固定缓冲区大小: 20 + retryCount: 3, // 固定重试次数: 3 + retryInterval: 3 * time.Second, // 固定重试间隔: 3秒 + stopChan: make(chan struct{}), + } + + // 启动异步发送协程 + sender.Start() + + return sender, nil +} + +// Start 启动异步发送协程 +func (s *TelegramSender) Start() { + s.wg.Add(1) + go s.listenAndSend() +} + +// SendAsync 异步发送消息(非阻塞) +func (s *TelegramSender) SendAsync(message string) { + select { + case s.msgChan <- message: + // 成功写入缓冲区 + default: + // 缓冲区满,丢弃消息(不阻塞主流程) + fmt.Printf("[Telegram] 消息缓冲区已满,消息被丢弃\n") + } +} + +// listenAndSend 监听channel并发送消息 +func (s *TelegramSender) listenAndSend() { + defer s.wg.Done() + + for { + select { + case msg := <-s.msgChan: + s.sendWithRetry(msg) + case <-s.stopChan: + // 清空缓冲区后退出 + for len(s.msgChan) > 0 { + msg := <-s.msgChan + s.sendWithRetry(msg) + } + return + } + } +} + +// sendWithRetry 发送消息(带重试) +func (s *TelegramSender) sendWithRetry(message string) { + var err error + for i := 0; i < s.retryCount; i++ { + err = s.send(message) + if err == nil { + return // 发送成功 + } + + // 重试前等待 + if i < s.retryCount-1 { + time.Sleep(s.retryInterval) + } + } + + // 所有重试都失败 + if err != nil { + fmt.Printf("[Telegram] 发送消息失败(已重试%d次): %v\n", s.retryCount, err) + } +} + +// send 发送单条消息 +func (s *TelegramSender) send(message string) error { + msg := tgbotapi.NewMessage(s.chatID, message) + msg.ParseMode = tgbotapi.ModeMarkdown + + _, err := s.bot.Send(msg) + return err +} + +// Stop 停止发送器(优雅关闭) +func (s *TelegramSender) Stop() { + s.once.Do(func() { + close(s.stopChan) + s.wg.Wait() + }) +} diff --git a/main.go b/main.go index 30c2abc9..40ad8136 100644 --- a/main.go +++ b/main.go @@ -25,54 +25,64 @@ type LeverageConfig struct { // ConfigFile 配置文件结构,只包含需要同步到数据库的字段 type ConfigFile struct { - AdminMode bool `json:"admin_mode"` - BetaMode bool `json:"beta_mode"` - APIServerPort int `json:"api_server_port"` - UseDefaultCoins bool `json:"use_default_coins"` - DefaultCoins []string `json:"default_coins"` - CoinPoolAPIURL string `json:"coin_pool_api_url"` - OITopAPIURL string `json:"oi_top_api_url"` - MaxDailyLoss float64 `json:"max_daily_loss"` - MaxDrawdown float64 `json:"max_drawdown"` - StopTradingMinutes int `json:"stop_trading_minutes"` - Leverage LeverageConfig `json:"leverage"` - JWTSecret string `json:"jwt_secret"` - DataKLineTime string `json:"data_k_line_time"` + AdminMode bool `json:"admin_mode"` + BetaMode bool `json:"beta_mode"` + APIServerPort int `json:"api_server_port"` + UseDefaultCoins bool `json:"use_default_coins"` + DefaultCoins []string `json:"default_coins"` + CoinPoolAPIURL string `json:"coin_pool_api_url"` + OITopAPIURL string `json:"oi_top_api_url"` + MaxDailyLoss float64 `json:"max_daily_loss"` + MaxDrawdown float64 `json:"max_drawdown"` + StopTradingMinutes int `json:"stop_trading_minutes"` + Leverage LeverageConfig `json:"leverage"` + JWTSecret string `json:"jwt_secret"` + DataKLineTime string `json:"data_k_line_time"` + Log *config.LogConfig `json:"log"` // 日志配置 } -// syncConfigToDatabase 从config.json读取配置并同步到数据库 -func syncConfigToDatabase(database config.DatabaseInterface) error { +// loadConfigFile 读取并解析config.json文件 +func loadConfigFile() (*ConfigFile, error) { // 检查config.json是否存在 if _, err := os.Stat("config.json"); os.IsNotExist(err) { - log.Printf("📄 config.json不存在,跳过同步") - return nil + log.Printf("📄 config.json不存在,使用默认配置") + return &ConfigFile{}, nil } // 读取config.json data, err := os.ReadFile("config.json") if err != nil { - return fmt.Errorf("读取config.json失败: %w", err) + return nil, fmt.Errorf("读取config.json失败: %w", err) } // 解析JSON var configFile ConfigFile if err := json.Unmarshal(data, &configFile); err != nil { - return fmt.Errorf("解析config.json失败: %w", err) + return nil, fmt.Errorf("解析config.json失败: %w", err) + } + + return &configFile, nil +} + +// syncConfigToDatabase 将配置同步到数据库 +func syncConfigToDatabase(database config.DatabaseInterface, configFile *ConfigFile) error { + if configFile == nil { + return nil } log.Printf("🔄 开始同步config.json到数据库...") // 同步各配置项到数据库 configs := map[string]string{ - "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), - "beta_mode": fmt.Sprintf("%t", configFile.BetaMode), - "api_server_port": strconv.Itoa(configFile.APIServerPort), - "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), - "coin_pool_api_url": configFile.CoinPoolAPIURL, - "oi_top_api_url": configFile.OITopAPIURL, - "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), - "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), - "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), + "admin_mode": fmt.Sprintf("%t", configFile.AdminMode), + "beta_mode": fmt.Sprintf("%t", configFile.BetaMode), + "api_server_port": strconv.Itoa(configFile.APIServerPort), + "use_default_coins": fmt.Sprintf("%t", configFile.UseDefaultCoins), + "coin_pool_api_url": configFile.CoinPoolAPIURL, + "oi_top_api_url": configFile.OITopAPIURL, + "max_daily_loss": fmt.Sprintf("%.1f", configFile.MaxDailyLoss), + "max_drawdown": fmt.Sprintf("%.1f", configFile.MaxDrawdown), + "stop_trading_minutes": strconv.Itoa(configFile.StopTradingMinutes), } // 同步default_coins(转换为JSON字符串存储) @@ -112,7 +122,7 @@ func syncConfigToDatabase(database config.DatabaseInterface) error { // loadBetaCodesToDatabase 加载内测码文件到数据库 func loadBetaCodesToDatabase(database config.DatabaseInterface) error { betaCodeFile := "beta_codes.txt" - + // 检查内测码文件是否存在 if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) { log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile) @@ -126,7 +136,7 @@ func loadBetaCodesToDatabase(database config.DatabaseInterface) error { } log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024) - + // 加载内测码到数据库 err = database.LoadBetaCodesFromFile(betaCodeFile) if err != nil { @@ -156,6 +166,12 @@ func main() { dbPath = os.Args[1] } + // 读取配置文件 + configFile, err := loadConfigFile() + if err != nil { + log.Fatalf("❌ 读取config.json失败: %v", err) + } + log.Printf("📋 初始化配置数据库: %s", dbPath) database, err := config.NewDatabase(dbPath) if err != nil { @@ -164,7 +180,7 @@ func main() { defer database.Close() // 同步config.json到数据库 - if err := syncConfigToDatabase(database); err != nil { + if err := syncConfigToDatabase(database, configFile); err != nil { log.Printf("⚠️ 同步config.json到数据库失败: %v", err) } diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 86c47db8..483421cb 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -23,9 +23,9 @@ type CompetitionCache struct { // TraderManager 管理多个trader实例 type TraderManager struct { - traders map[string]*trader.AutoTrader // key: trader ID + traders map[string]*trader.AutoTrader // key: trader ID competitionCache *CompetitionCache - mu sync.RWMutex + mu sync.RWMutex } // NewTraderManager 创建trader管理器 @@ -170,7 +170,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterfa } // 添加到TraderManager - err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) + err = tm.addTraderFromDB(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins, database, traderCfg.UserID) if err != nil { log.Printf("❌ 添加交易员 %s 失败: %v", traderCfg.Name, err) continue @@ -182,7 +182,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterfa } // addTraderFromConfig 内部方法:从配置添加交易员(不加锁,因为调用方已加锁) -func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error { +func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database config.DatabaseInterface, userID string) error { if _, exists := tm.traders[traderCfg.ID]; exists { return fmt.Errorf("trader ID '%s' 已存在", traderCfg.ID) } @@ -262,7 +262,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel } // 创建trader实例 - at, err := trader.NewAutoTrader(traderConfig) + at, err := trader.NewAutoTrader(traderConfig, database, userID) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } @@ -286,7 +286,7 @@ func (tm *TraderManager) addTraderFromDB(traderCfg *config.TraderRecord, aiModel // AddTrader 从数据库配置添加trader (移除旧版兼容性) // AddTraderFromDB 从数据库配置添加trader -func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error { +func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database *config.Database, userID string) error { tm.mu.Lock() defer tm.mu.Unlock() @@ -368,7 +368,7 @@ func (tm *TraderManager) AddTraderFromDB(traderCfg *config.TraderRecord, aiModel } // 创建trader实例 - at, err := trader.NewAutoTrader(traderConfig) + at, err := trader.NewAutoTrader(traderConfig, database, userID) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } @@ -506,19 +506,19 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { tm.competitionCache.mu.RUnlock() tm.mu.RLock() - + // 获取所有交易员列表 allTraders := make([]*trader.AutoTrader, 0, len(tm.traders)) for _, t := range tm.traders { allTraders = append(allTraders, t) } tm.mu.RUnlock() - + log.Printf("🔄 重新获取竞赛数据,交易员数量: %d", len(allTraders)) - + // 并发获取交易员数据 traders := tm.getConcurrentTraderData(allTraders) - + // 按收益率排序(降序) sort.Slice(traders, func(i, j int) bool { pnlPctI, okI := traders[i]["total_pnl_pct"].(float64) @@ -531,14 +531,14 @@ func (tm *TraderManager) GetCompetitionData() (map[string]interface{}, error) { } return pnlPctI > pnlPctJ }) - + // 限制返回前50名 totalCount := len(traders) limit := 50 if len(traders) > limit { traders = traders[:limit] } - + comparison := make(map[string]interface{}) comparison["traders"] = traders comparison["count"] = len(traders) @@ -559,21 +559,21 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ index int data map[string]interface{} } - + // 创建结果通道 resultChan := make(chan traderResult, len(traders)) - + // 并发获取每个交易员的数据 for i, t := range traders { go func(index int, trader *trader.AutoTrader) { // 设置单个交易员的超时时间为3秒 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - + // 使用通道来实现超时控制 accountChan := make(chan map[string]interface{}, 1) errorChan := make(chan error, 1) - + go func() { account, err := trader.GetAccountInfo() if err != nil { @@ -582,10 +582,10 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ accountChan <- account } }() - + status := trader.GetStatus() var traderData map[string]interface{} - + select { case account := <-accountChan: // 成功获取账户信息 @@ -634,18 +634,18 @@ func (tm *TraderManager) getConcurrentTraderData(traders []*trader.AutoTrader) [ "error": "获取超时", } } - + resultChan <- traderResult{index: index, data: traderData} }(i, t) } - + // 收集所有结果 results := make([]map[string]interface{}, len(traders)) for i := 0; i < len(traders); i++ { result := <-resultChan results[result.index] = result.data } - + return results } @@ -656,20 +656,20 @@ func (tm *TraderManager) GetTopTradersData() (map[string]interface{}, error) { if err != nil { return nil, err } - + // 从竞赛数据中提取前5名 allTraders, ok := competitionData["traders"].([]map[string]interface{}) if !ok { return nil, fmt.Errorf("竞赛数据格式错误") } - + // 限制返回前5名 limit := 5 topTraders := allTraders if len(allTraders) > limit { topTraders = allTraders[:limit] } - + result := map[string]interface{}{ "traders": topTraders, "count": len(topTraders), @@ -832,7 +832,7 @@ func (tm *TraderManager) LoadUserTraders(database config.DatabaseInterface, user } // 使用现有的方法加载交易员 - err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins) + err = tm.loadSingleTrader(traderCfg, aiModelCfg, exchangeCfg, coinPoolURL, oiTopURL, maxDailyLoss, maxDrawdown, stopTradingMinutes, defaultCoins, database, userID) if err != nil { log.Printf("⚠️ 加载交易员 %s 失败: %v", traderCfg.Name, err) } @@ -842,7 +842,7 @@ func (tm *TraderManager) LoadUserTraders(database config.DatabaseInterface, user } // loadSingleTrader 加载单个交易员(从现有代码提取的公共逻辑) -func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string) error { +func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiModelCfg *config.AIModelConfig, exchangeCfg *config.ExchangeConfig, coinPoolURL, oiTopURL string, maxDailyLoss, maxDrawdown float64, stopTradingMinutes int, defaultCoins []string, database config.DatabaseInterface, userID string) error { // 处理交易币种列表 var tradingCoins []string if traderCfg.TradingSymbols != "" { @@ -889,6 +889,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode DefaultCoins: defaultCoins, TradingCoins: tradingCoins, SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 + HyperliquidTestnet: exchangeCfg.Testnet, // Hyperliquid测试网 } // 根据交易所类型设置API密钥 @@ -912,7 +913,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode } // 创建trader实例 - at, err := trader.NewAutoTrader(traderConfig) + at, err := trader.NewAutoTrader(traderConfig, database, userID) if err != nil { return fmt.Errorf("创建trader失败: %w", err) } diff --git a/market/monitor.go b/market/monitor.go index 23e126d9..340613ac 100644 --- a/market/monitor.go +++ b/market/monitor.go @@ -121,19 +121,19 @@ func (m *WSMonitor) Start(coins []string) { // 初始化交易对 err := m.Initialize(coins) if err != nil { - log.Fatalf("❌ 初始化币种: %v", err) + log.Printf("❌ 初始化币种失败: %v", err) return } err = m.combinedClient.Connect() if err != nil { - log.Fatalf("❌ 批量订阅流: %v", err) + log.Printf("❌ 批量订阅流失败: %v", err) return } // 订阅所有交易对 err = m.subscribeAll() if err != nil { - log.Fatalf("❌ 订阅币种交易对: %v", err) + log.Printf("❌ 订阅币种交易对失败: %v", err) return } } @@ -159,7 +159,7 @@ func (m *WSMonitor) subscribeAll() error { for _, st := range subKlineTime { err := m.combinedClient.BatchSubscribeKlines(m.symbols, st) if err != nil { - log.Fatalf("❌ 订阅3m K线: %v", err) + log.Printf("❌ 订阅 %s K线失败: %v", st, err) return err } } @@ -239,19 +239,32 @@ func (m *WSMonitor) GetCurrentKlines(symbol string, _time string) ([]Kline, erro // 如果Ws数据未初始化完成时,单独使用api获取 - 兼容性代码 (防止在未初始化完成是,已经有交易员运行) apiClient := NewAPIClient() klines, err := apiClient.GetKlines(symbol, _time, 100) - m.getKlineDataMap(_time).Store(strings.ToUpper(symbol), klines) //动态缓存进缓存 + if err != nil { + return nil, fmt.Errorf("获取%v分钟K线失败: %v", _time, err) + } + + // 动态缓存进缓存 + m.getKlineDataMap(_time).Store(strings.ToUpper(symbol), klines) + + // 订阅 WebSocket 流 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) + log.Printf("警告: 动态订阅%v分钟K线失败: %v (使用API数据)", _time, subErr) } - if err != nil { - return nil, fmt.Errorf("获取%v分钟K线失败: %v", _time, err) - } - return klines, fmt.Errorf("symbol不存在") + + // ✅ FIX: 返回深拷贝而非引用 + result := make([]Kline, len(klines)) + copy(result, klines) + return result, nil } - return value.([]Kline), nil + + // ✅ FIX: 返回深拷贝而非引用,避免并发竞态条件 + klines := value.([]Kline) + result := make([]Kline, len(klines)) + copy(result, klines) + return result, nil } func (m *WSMonitor) Close() { diff --git a/mcp/client.go b/mcp/client.go index 9191dfaf..14f49eae 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -7,6 +7,8 @@ import ( "io" "log" "net/http" + "os" + "strconv" "strings" "time" ) @@ -28,15 +30,28 @@ type Client struct { Model string Timeout time.Duration UseFullURL bool // 是否使用完整URL(不添加/chat/completions) + MaxTokens int // AI响应的最大token数 } func New() *Client { + // 从环境变量读取 MaxTokens,默认 2000 + maxTokens := 2000 + if envMaxTokens := os.Getenv("AI_MAX_TOKENS"); envMaxTokens != "" { + if parsed, err := strconv.Atoi(envMaxTokens); err == nil && parsed > 0 { + maxTokens = parsed + log.Printf("🔧 [MCP] 使用环境变量 AI_MAX_TOKENS: %d", maxTokens) + } else { + log.Printf("⚠️ [MCP] 环境变量 AI_MAX_TOKENS 无效 (%s),使用默认值: %d", envMaxTokens, maxTokens) + } + } + // 默认配置 return &Client{ - Provider: ProviderDeepSeek, - BaseURL: "https://api.deepseek.com/v1", - Model: "deepseek-chat", - Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 + Provider: ProviderDeepSeek, + BaseURL: "https://api.deepseek.com/v1", + Model: "deepseek-chat", + Timeout: 120 * time.Second, // 增加到120秒,因为AI需要分析大量数据 + MaxTokens: maxTokens, } } @@ -81,7 +96,7 @@ func (client *Client) SetQwenAPIKey(apiKey string, customURL string, customModel client.Model = customModel log.Printf("🔧 [MCP] Qwen 使用自定义 Model: %s", customModel) } else { - client.Model = "qwen-plus" // 可选: qwen-turbo, qwen-plus, qwen-max + client.Model = "qwen3-max" log.Printf("🔧 [MCP] Qwen 使用默认 Model: %s", client.Model) } // 打印 API Key 的前后各4位用于验证 @@ -190,7 +205,7 @@ func (client *Client) callOnce(systemPrompt, userPrompt string) (string, error) "model": client.Model, "messages": messages, "temperature": 0.5, // 降低temperature以提高JSON格式稳定性 - "max_tokens": 2000, + "max_tokens": client.MaxTokens, } // 注意:response_format 参数仅 OpenAI 支持,DeepSeek/Qwen 不支持 @@ -280,6 +295,8 @@ func isRetryableError(err error) bool { "connection refused", "temporary failure", "no such host", + "stream error", // HTTP/2 stream 错误 + "INTERNAL_ERROR", // 服务端内部错误 } for _, retryable := range retryableErrors { if strings.Contains(errStr, retryable) { diff --git a/prompts/Hansen.txt b/prompts/Hansen.txt new file mode 100644 index 00000000..68815c77 --- /dev/null +++ b/prompts/Hansen.txt @@ -0,0 +1,180 @@ +你是专业的加密货币AI,在合约市场进行自主交易。 + +# 核心目标 + +**最大化夏普比率(Sharpe Ratio)** + +夏普比率 = 平均收益 / 收益波动率 + +**这意味着**: +- 高质量交易(高胜率、大盈亏比)→ 提升夏普 +- 稳定收益、控制回撤 → 提升夏普 +- 耐心持仓、让利润奔跑 → 提升夏普 +- 频繁交易、小盈小亏 → 增加波动,严重降低夏普 +- 过度交易、手续费损耗 → 直接亏损 +- 过早平仓、频繁进出 → 错失大行情 + +**关键认知**: 系统每3分钟扫描一次,但不意味着每次都要交易! +大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 + +# 交易哲学 & 最佳实践 + +## 核心原则: + +**资金保全第一**:保护资本比追求收益更重要 - 这是最高原则 + +**纪律胜于情绪**:严格执行退出策略,不随意移动止损或目标 + +**质量优于数量**:少量高信念交易胜过大量低信念交易 + +**适应波动性**:根据市场条件调整仓位大小和杠杆 + +**尊重趋势**:不要与强趋势作对,顺势而为 + +**风险控制优先**:每笔交易必须明确止损点和风险金额 + +## 稳健交易行为准则: + +**等待最佳机会**:宁可错过10个普通机会,不错过1个优质机会 +**分批止盈**:在关键阻力位分批获利了结 +**严格止损**:入场前就设定好止损,绝不移动止损扩大风险 +**仓位匹配**:根据信号强度调整仓位,不强求固定仓位 +**情绪控制**:连续盈利不骄傲,连续亏损不报复 + +## 常见误区避免: + +**过度交易**:频繁交易导致费用侵蚀利润 +**复仇式交易**:亏损后立即加码试图"翻本" +**分析瘫痪**:过度等待完美信号,导致失机 +**忽视相关性**:BTC常引领山寨币,须优先观察BTC趋势 +**过度杠杆**:放大收益同时放大亏损 +**逆势操作**:在强趋势中反向交易 + +# 交易频率认知 + +**量化标准**: +- 优秀交易员:每天2-4笔 = 每小时0.1-0.2笔 +- 过度交易:每小时>2笔 = 严重问题 +- 最佳节奏:开仓后持有至少30-60分钟 + +**稳健自查**: +- 如果你发现自己每个周期都在交易 → 说明标准太低 +- 如果你发现持仓<30分钟就平仓 → 说明太急躁 +- 如果连续3个周期没有合适机会 → 这是正常现象 +- 如果感觉"必须交易" → 立即停止,这是危险信号 + +# 开仓标准(严格) + +只在**强信号**时开仓,不确定就观望。 + +## 多维度信号验证: + +**趋势确认**(必须满足): +- 4小时级别趋势明确 +- 价格在关键EMA(20/50)之上/之下 +- 至少2个时间框架方向一致 + +**技术指标**(至少满足3项): +- MACD方向与趋势一致 +- RSI在合理区域(不做超买区做多/超卖区做空) +- 成交量配合价格方向 +- 持仓量变化支持趋势 + +**入场时机**: +- 回撤至支撑/阻力位 +- 突破关键水平后回踩确认 +- 形态完成(头肩、三角、旗形等) + +**风险控制**: +- 止损位置明确且合理 +- 风险回报比 ≥ 1:3 +- 单笔风险 ≤ 账户2% + +## 避免开仓的情况: + +横盘震荡,无明确方向 +重大事件前后(不确定性高) +流动性不足时段 +刚平仓不久(<15分钟) +情绪化状态(急于翻本或过度自信) +多个指标相互矛盾 + +# 夏普比率自我进化 + +每次你会收到**夏普比率**作为绩效反馈: + +**夏普比率 < -0.5** (持续亏损): + → **停止交易**,连续观望至少6个周期(18分钟) + → **深度反思**: + • 交易频率过高?(每小时>1次就是过度) + • 持仓时间过短?(<30分钟就是过早平仓) + • 信号强度不足?(信心度<80) + • 是否逆势操作? + • 止损执行是否严格? + +**夏普比率 -0.5 ~ 0** (轻微亏损): + → **严格控制**:只做信心度>85的交易 + → 减少交易频率:每小时最多1笔新开仓 + → 缩小仓位:使用正常仓位的50-70% + → 耐心持仓:至少持有45分钟以上 + +**夏普比率 0 ~ 0.7** (正收益): + → **维持策略**:按既定标准执行 + → 保持警惕:不因盈利而放松标准 + +**夏普比率 > 0.7** (优异表现): + → **适度进取**:可在信心度>90时适度扩大仓位 + → 保持纪律:不因成功而改变稳健原则 + +# 决策流程 + +1. **分析账户状态**: + - 当前夏普比率表现 + - 保证金使用情况 + - 持仓数量和状态 + +2. **评估市场环境**: + - BTC整体趋势方向 + - 市场波动率和情绪 + - 重大事件风险 + +3. **检查现有持仓**: + - 趋势是否持续? + - 是否需要调整止损/止盈? + - 是否达到目标位? + +4. **寻找新机会**(仅在条件允许时): + - 多维度信号验证 + - 风险回报比计算 + - 仓位规模确定 + +5. **输出决策**:思维链分析 + 完整的JSON + +# 风险控制框架 + +## 仓位管理: +- 单币种风险:≤ 账户净值的2% +- 总仓位风险:≤ 账户净值的6% +- 最大持仓:3个币种 +- 杠杆使用:根据波动性调整,不追求最大杠杆 + +## 止损策略: +- 技术止损:基于支撑/阻力位 +- 金额止损:单笔最大亏损金额 +- 时间止损:持仓超过2小时无进展考虑离场 + +## 资金保护: +- 连续2笔亏损后:降低50%仓位 +- 单日亏损超过5%:停止交易剩余时间 +- 每周亏损超过10%:全面复盘策略 + +--- + +**记住**: +- 目标是夏普比率,不是交易频率 +- 资金保全比利润追求更重要 +- 宁可错过,不做低质量交易 +- 风险回报比1:3是底线 +- 纪律执行是长期盈利的关键 + +**现在,请基于以上原则分析市场并做出稳健决策** diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index d5778caa..7b62968a 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -61,21 +61,24 @@ ## 开平仓动作 -1. **buy_to_enter**: 开多仓(看涨) +1. **open_long**: 开多仓(看涨) - 用于: 看涨信号强烈时 - 必须设置: 止损价格、止盈价格 -2. **sell_to_enter**: 开空仓(看跌) +2. **open_short**: 开空仓(看跌) - 用于: 看跌信号强烈时 - 必须设置: 止损价格、止盈价格 -3. **close**: 完全平仓 - - 用于: 止盈、止损、或趋势反转 +3. **close_long**: 平掉多仓 + - 用于: 止盈、止损、或趋势反转(针对多头持仓) -4. **wait**: 观望,不持仓 +4. **close_short**: 平掉空仓 + - 用于: 止盈、止损、或趋势反转(针对空头持仓) + +5. **wait**: 观望,不持仓 - 用于: 没有明确信号,或资金不足 -5. **hold**: 持有当前仓位 +6. **hold**: 持有当前仓位 - 用于: 持仓表现符合预期,继续等待 ## 动态调整动作 (新增) @@ -97,6 +100,15 @@ --- +# 动态止盈止损与部分平仓指引 + +- `partial_close` 用于锁定阶段性收益或降低风险,建议使用清晰比例(如 25% / 50% / 75%),并说明目的(例:"锁定关键阻力前利润""减半仓等待回踩确认")。 +- 执行部分平仓后,应评估是否需要同步上调止损 / 下调止盈,确保剩余仓位符合新的风险回报结构。 +- `update_stop_loss` / `update_take_profit` 优先用于顺势推进(如跟踪新高新低),避免在无新证据下放宽止损。 +- 若计划分批退出,请在 `reasoning` 中描述剩余仓位的策略与失效条件,避免出现"减仓后不知道如何处理剩余部位"的情况。 + +--- + # 决策流程(严格顺序) ## 第 0 步:疑惑检查 @@ -330,26 +342,25 @@ ## 仓位计算公式 -``` -仓位大小(USD) = 可用资金 × 风险预算 / 止损距离百分比 -仓位数量(Coins) = 仓位大小(USD) / 当前价格 -``` +**重要**:position_size_usd 是**名义价值**(包含杠杆),非保证金需求。 -**示例**: -``` -账户净值:10,000 USDT -风险预算:2%(信心度 90-95) -止损距离:2%(50,000 → 49,000) +**计算步骤**: +1. **可用保证金** = Available Cash × 0.95 × Allocation %(预留5%给手续费) +2. **名义价值** = 可用保证金 × Leverage +3. **position_size_usd** = 名义价值(这是 JSON 中应填写的值) +4. **Position Size (Coins)** = position_size_usd / Current Price -仓位大小 = 10,000 × 2% / 2% = 10,000 USDT -杠杆 5x → 保证金 2,000 USDT -``` +**示例**:Available Cash = $500, Leverage = 5x, Allocation = 100% +- 可用保证金 = $500 × 0.95 × 100% = $475 +- position_size_usd = $475 × 5 = **$2,375** ← JSON 中填写此值 +- 实际占用保证金 = $475,剩余 $25 用于手续费 -## 杠杆选择指南 +## 杠杆选择指引 -- 信心度 85-87: 3-5x 杠杆 -- 信心度 88-92: 5-10x 杠杆 -- 信心度 93-95: 10-15x 杠杆 +基于信心度的杠杆配置: +- 信心度 <85 → 不开仓 +- 信心度 85-90 → 杠杆 1-3x,风险预算 1.5% +- 信心度 90-95 → 杠杆 3-8x,风险预算 2% - 信心度 >95: 最高 20x 杠杆(谨慎) ## 风险控制原则 diff --git a/prompts/adaptive_relaxed.txt b/prompts/adaptive_relaxed.txt new file mode 100644 index 00000000..3d77b5c3 --- /dev/null +++ b/prompts/adaptive_relaxed.txt @@ -0,0 +1,194 @@ +你是专业的加密货币交易AI,在合约市场进行自主交易。 + +# 核心目标 + +最大化夏普比率(Sharpe Ratio) + +夏普比率 = 平均收益 / 收益波动率 + +这意味着: +- 高质量交易(高胜率、大盈亏比)→ 提升夏普 +- 稳定收益、控制回撤 → 提升夏普 +- 耐心持仓、让利润奔跑 → 提升夏普 +- 频繁交易、小盈小亏 → 增加波动,严重降低夏普 +- 过度交易、手续费损耗 → 直接亏损 + +关键认知:系统每3分钟扫描一次,但不意味着每次都要交易! +大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 + +--- + +# 零号原则:疑惑优先 + +⚠️ 当你不确定时,默认选择 `wait`。 + +这是覆盖所有其他规则的最高优先级: +- 任何环节产生疑虑 → 立刻选择 `wait` +- 只有当信心 ≥80 且论据充分、条件完全满足时才允许开仓(✅ 从85降至80) +- 不确定是否违规 → 视同违规,直接 `wait` + +--- + +# 基础交易约束 + +- 禁止对同一标的同时持有多空(NO hedging) +- 禁止在既有仓位上加码(NO pyramiding) +- 允许使用 `partial_close` 锁定利润或降低风险 +- 每笔交易必须预先设定止损与止盈,止损允许的账户亏损不超过 1-3% +- 确保预估清算价距离 ≥15%,避免被强平 + +--- + +# 仓位管理框架 + +## 杠杆选择指引 + +基于信心度的杠杆配置: +- 信心度 <80 → 不开仓(✅ 从85降至80) +- 信心度 80-85 → 杠杆 1-3x,风险预算 1.5% +- 信心度 85-92 → 杠杆 3-5x,风险预算 2% +- 信心度 >92 → 杠杆 5-8x(谨慎),风险预算 2.5% + +--- + +# 决策流程(强制顺序) + +1. **冷却期检查** + - 距离上一次开仓 ≥6 分钟(✅ 从9分钟降至6分钟) + - 若有持仓:持仓时间 ≥20 分钟(✅ 从30分钟降至20分钟) + - 止损出场后至少观望 6 分钟 + → 任意条件不满足 → `action = "wait"` + +2. **夏普 / 连亏防御** + - 夏普 < -0.5 → 停手 6 个周期(18 分钟) + - 连续 2 次亏损 → 暂停 30 分钟(✅ 从45分钟降至30分钟) + - 连续 3 次亏损 → 暂停 12 小时(✅ 从24小时降至12小时) + - 连续 4 次亏损 → 暂停 48 小时(✅ 从72小时降至48小时) + +3. **持仓管理优先** + - 若已有持仓:先评估是否需要平仓或调整止盈止损 + +4. **BTC 状态评估(若数据可用)** + - 标准模式:拥有 15m / 1h / 4h → 至少两条周期同向且无矛盾视为支持 + - 简化模式:仅 15m / 4h → 同向视为支持 + - 若完全缺少 BTC 数据 → 跳过此步,但开仓信心阈值上调至 85 + +5. **多周期趋势确认**(✅ 降低要求) + + 开仓前必须验证多周期趋势一致性: + + **做多时检查**: + - 检查 3m / 15m / 1h / 4h 的价格与 EMA20 关系 + - 至少 2 个周期显示价格 > EMA20(✅ 从3个降至2个) + - 4h MACD ≥ -0.5(✅ 从-0.2放宽至-0.5) + + **做空时检查**: + - 至少 2 个周期显示价格 < EMA20(✅ 从3个降至2个) + - 4h MACD ≤ +0.5(✅ 从+0.2放宽至+0.5) + + **趋势共振评分**: + - 4 个周期全部同向 → 趋势极强(信心 +10) + - 3 个周期同向 → 趋势确认(信心 +5) + - 2 个周期同向 → 趋势可接受(允许开仓) + +6. **新机会评估** + - 多空确认清单 ≥4/8 项通过(✅ 从5/8降至4/8) + - 风险回报比 ≥1:2.5(✅ 从1:3降至1:2.5) + - 预计收益 > 手续费 ×3 + - 清算距离 ≥15% + - 信心评分 ≥80(若跳过 BTC 检查则 ≥85) + +--- + +# 多空确认清单(至少通过 4/8)(✅ 降低要求) + +### 做多确认 + +| 指标 | 条件 | +|------|------| +| 15m MACD | >0(短期动能向上) | +| 价格 vs EMA20 | 价格高于 15m / 1h EMA20 | +| RSI | <45(超卖或温和超卖)(✅ 从30-40放宽至<45) | +| BuySellRatio | ≥0.55(✅ 从0.60降至0.55) | +| 成交量 | 近 20 根均量 ×1.3 以上(✅ 从1.5降至1.3) | +| BTC 状态* | 多头或中性 | +| 资金费率 | <0.02 或 -0.01~0.02 | +| 持仓量 OI 变化 | 近 4 小时上升 >+3%(✅ 从+5%降至+3%) | + +### 做空确认 + +| 指标 | 条件 | +|------|------| +| 15m MACD | <0(短期动能向下) | +| 价格 vs EMA20 | 价格低于 15m / 1h EMA20 | +| RSI | >60(超买或温和超买)(✅ 从65-70放宽至>60) | +| BuySellRatio | ≤0.45(✅ 从0.40提高至0.45) | +| 成交量 | 近 20 根均量 ×1.3 以上 | +| BTC 状态* | 空头或中性 | +| 资金费率 | >-0.02 或 -0.02~0.01 | +| 持仓量 OI 变化 | 近 4 小时上升 >+3% | + +--- + +# 客观信心评分(基础分 60) + +1. **基础分:60** +2. **加分项(每项 +5,最高 100)** + - 多空确认清单 ≥4 项通过 + - BTC 状态明确支持 + - 多周期趋势共振(2 个周期同向 +3,3 个周期同向 +5,4 个周期全同向 +10) + - 15m / 1h / 4h MACD 同向 + - 关键技术位明确(1h / 4h EMA、整数关口) + - 成交量放大(>1.3× 均量) + - 资金费率情绪背离 + - 风险回报 ≥1:3 +3. **减分项(每项 -10)** + - 指标互相矛盾(MACD 与价格背离) + - BTC 状态不明仍计划大幅开仓 + - 技术位不清晰或过近(<0.5%) + - 成交量萎缩(< 均量 ×0.7) +4. **阈值规则** + - <80 → 禁止开仓 + - 80-85 → 风险预算 1.5%,杠杆 1-3x + - 85-92 → 风险预算 2%,杠杆 3-5x + - >92 → 风险预算 2.5%,杠杆 5-8x + +--- + +# 最终检查清单(开仓前必须全部通过) + +1. 冷却期合格(6分钟) +2. 夏普 / 连亏未触发停手 +3. **多周期趋势确认通过(至少 2 个周期同向)** +4. BTC 状态明确支持(或缺失时已说明并提高阈值) +5. 多空确认清单 ≥4/8 +6. 风险回报 ≥1:2.5 +7. 预计收益 > 手续费 ×3 +8. 清算距离 ≥15% +9. 客观信心评分 ≥80(缺 BTC 数据时 ≥85) +10. 失效条件已定义且写入 reasoning + +任意一项未通过 → 立即选择 `wait`,并说明具体原因。 + +--- + +## 版本说明 + +**adaptive_relaxed v1.0 - 保守优化版** + +核心调整: +1. ✅ 信心度阈值:85 → 80 +2. ✅ 冷却期:9分钟 → 6分钟 +3. ✅ 多周期趋势:3个同向 → 2个同向 +4. ✅ 多空确认清单:5/8 → 4/8 +5. ✅ RSI 放宽:30-40/65-70 → <45/>60 +6. ✅ BuySellRatio 放宽:0.60/0.40 → 0.55/0.45 +7. ✅ 成交量要求:1.5× → 1.3× +8. ✅ OI 变化:+5% → +3% +9. ✅ 风险回报比:1:3 → 1:2.5 + +预期效果: +- 交易频率增加 50-80%(一天 8-15 笔) +- 保持 50%+ 胜率 +- 允许更多山寨币机会 +- 保持核心風控(夏普、連虧停手) diff --git a/prompts/default.txt b/prompts/default.txt index 310978ac..3094e473 100644 --- a/prompts/default.txt +++ b/prompts/default.txt @@ -106,6 +106,21 @@ 3. 寻找新机会: 有强信号吗?多空机会? 4. 输出决策: 思维链分析 + JSON +# 仓位大小计算 + +**重要**:`position_size_usd` 是**名义价值**(包含杠杆),非保证金需求。 + +**计算步骤**: +1. **可用保证金** = Available Cash × 0.95 × 配置比例(预留5%手续费) +2. **名义价值** = 可用保证金 × Leverage +3. **position_size_usd** = 名义价值(JSON中填写此值) +4. **实际币数** = position_size_usd / Current Price + +**示例**:可用资金 $500,杠杆 5x,配置 100% +- 可用保证金 = $500 × 0.95 = $475 +- position_size_usd = $475 × 5 = **$2,375** ← JSON填此值 +- 实际占用保证金 = $475,剩余 $25 用于手续费 + --- 记住: diff --git a/prompts/nof1.txt b/prompts/nof1.txt index 012daa62..ef9f797d 100644 --- a/prompts/nof1.txt +++ b/prompts/nof1.txt @@ -21,19 +21,25 @@ Your mission: Maximize risk-adjusted returns (PnL) through systematic, disciplin # ACTION SPACE DEFINITION -You have exactly FOUR possible actions per decision cycle: +You have exactly SIX possible actions per decision cycle: -1. **buy_to_enter**: Open a new LONG position (bet on price appreciation) +1. **open_long**: Open a new LONG position (bet on price appreciation) - Use when: Bullish technical setup, positive momentum, risk-reward favors upside -2. **sell_to_enter**: Open a new SHORT position (bet on price depreciation) +2. **open_short**: Open a new SHORT position (bet on price depreciation) - Use when: Bearish technical setup, negative momentum, risk-reward favors downside -3. **hold**: Maintain current positions without modification +3. **close_long**: Exit an existing LONG position entirely + - Use when: Profit target reached, stop loss triggered, or thesis invalidated (for long positions) + +4. **close_short**: Exit an existing SHORT position entirely + - Use when: Profit target reached, stop loss triggered, or thesis invalidated (for short positions) + +5. **hold**: Maintain current positions without modification - Use when: Existing positions are performing as expected, or no clear edge exists -4. **close**: Exit an existing position entirely - - Use when: Profit target reached, stop loss triggered, or thesis invalidated +6. **wait**: Do not open any new positions, no current holdings + - Use when: No clear trading signal or insufficient capital ## Position Management Constraints @@ -45,10 +51,19 @@ You have exactly FOUR possible actions per decision cycle: # POSITION SIZING FRAMEWORK -Calculate position size using this formula: +**IMPORTANT**: `position_size_usd` is the **notional value** (includes leverage), NOT margin requirement. -Position Size (USD) = Available Cash × Leverage × Allocation % -Position Size (Coins) = Position Size (USD) / Current Price +## Calculation Steps: + +1. **Available Margin** = Available Cash × 0.95 × Allocation % (reserve 5% for fees) +2. **Notional Value** = Available Margin × Leverage +3. **position_size_usd** = Notional Value (this is the value for JSON) +4. **Position Size (Coins)** = position_size_usd / Current Price + +**Example**: Available Cash = $500, Leverage = 5x, Allocation = 100% +- Available Margin = $500 × 0.95 × 100% = $475 +- position_size_usd = $475 × 5 = **$2,375** ← Fill this value in JSON +- Actual margin used = $475, remaining $25 for fees ## Sizing Considerations diff --git a/start.sh b/start.sh index 47cb2536..3c571067 100755 --- a/start.sh +++ b/start.sh @@ -165,6 +165,16 @@ start() { # 读取环境变量 read_env_vars + # 确保必要的文件和目录存在(修复 Docker volume 挂载问题) + if [ ! -f "config.db" ]; then + print_info "创建数据库文件..." + touch config.db + fi + if [ ! -d "decision_logs" ]; then + print_info "创建日志目录..." + mkdir -p decision_logs + fi + # Auto-build frontend if missing or forced # if [ ! -d "web/dist" ] || [ "$1" == "--build" ]; then # build_frontend diff --git a/trader/aster_trader.go b/trader/aster_trader.go index d9ba82a6..a4a5a41f 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -438,13 +438,23 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { return nil, err } + // 🔍 调试:打印原始API响应 + log.Printf("🔍 Aster API原始响应: %s", string(body)) + // 查找USDT余额 totalBalance := 0.0 availableBalance := 0.0 crossUnPnl := 0.0 for _, bal := range balances { + // 🔍 调试:打印每条余额记录 + log.Printf("🔍 余额记录: %+v", bal) + if asset, ok := bal["asset"].(string); ok && asset == "USDT" { + // 🔍 调试:打印USDT余额详情 + log.Printf("🔍 USDT余额详情: balance=%v, availableBalance=%v, crossUnPnl=%v", + bal["balance"], bal["availableBalance"], bal["crossUnPnl"]) + if wb, ok := bal["balance"].(string); ok { totalBalance, _ = strconv.ParseFloat(wb, 64) } @@ -458,11 +468,25 @@ func (t *AsterTrader) GetBalance() (map[string]interface{}, error) { } } + // ✅ Aster API完全兼容Binance API格式 + // balance字段 = wallet balance(不包含未实现盈亏) + // crossUnPnl = unrealized profit(未实现盈亏) + // crossWalletBalance = balance + crossUnPnl(全仓钱包余额,包含盈亏) + // + // 参考Binance官方文档: + // - Account Information V2: marginBalance = walletBalance + unrealizedProfit + // - Balance V3: crossWalletBalance = balance + crossUnPnl + + log.Printf("✓ Aster API返回: 钱包余额=%.2f, 未实现盈亏=%.2f, 可用余额=%.2f", + totalBalance, + crossUnPnl, + availableBalance) + // 返回与Binance相同的字段名,确保AutoTrader能正确解析 return map[string]interface{}{ - "totalWalletBalance": totalBalance, + "totalWalletBalance": totalBalance, // 钱包余额(不含未实现盈亏) "availableBalance": availableBalance, - "totalUnrealizedProfit": crossUnPnl, + "totalUnrealizedProfit": crossUnPnl, // 未实现盈亏 }, nil } @@ -842,6 +866,21 @@ func (t *AsterTrader) SetMarginMode(symbol string, isCrossMargin bool) error { log.Printf(" ✓ %s 仓位模式已是 %s 或有持仓无法更改", symbol, marginType) return nil } + // 检测多资产模式(错误码 -4168) + if strings.Contains(err.Error(), "Multi-Assets mode") || + strings.Contains(err.Error(), "-4168") || + strings.Contains(err.Error(), "4168") { + log.Printf(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol) + log.Printf(" 💡 提示:如需使用逐仓模式,请在交易所关闭多资产模式") + return nil + } + // 检测统一账户 API + if strings.Contains(err.Error(), "unified") || + strings.Contains(err.Error(), "portfolio") || + strings.Contains(err.Error(), "Portfolio") { + log.Printf(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol) + return fmt.Errorf("请使用「现货与合约交易」API 权限,不要使用「统一账户 API」") + } log.Printf(" ⚠️ 设置仓位模式失败: %v", err) // 不返回错误,让交易继续 return nil @@ -971,6 +1010,108 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity return err } + + +// CancelStopLossOrders 仅取消止损单(不影响止盈单) +func (t *AsterTrader) CancelStopLossOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止损订单(不取消止盈订单) + if orderType == "STOP_MARKET" || orderType == "STOP" { + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消止损单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s)", int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount) + } + + return nil +} + +// CancelTakeProfitOrders 仅取消止盈单(不影响止损单) +func (t *AsterTrader) CancelTakeProfitOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止盈单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止盈订单(不取消止损订单) + if orderType == "TAKE_PROFIT_MARKET" || orderType == "TAKE_PROFIT" { + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v1/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消止盈单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s)", int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount) + } + + return nil +} + // CancelAllOrders 取消所有订单 func (t *AsterTrader) CancelAllOrders(symbol string) error { params := map[string]interface{}{ @@ -981,6 +1122,61 @@ func (t *AsterTrader) CancelAllOrders(symbol string) error { return err } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *AsterTrader) CancelStopOrders(symbol string) error { + // 获取该币种的所有未完成订单 + params := map[string]interface{}{ + "symbol": symbol, + } + + body, err := t.request("GET", "/fapi/v3/openOrders", params) + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + var orders []map[string]interface{} + if err := json.Unmarshal(body, &orders); err != nil { + return fmt.Errorf("解析订单数据失败: %w", err) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType, _ := order["type"].(string) + + // 只取消止损和止盈订单 + if orderType == "STOP_MARKET" || + orderType == "TAKE_PROFIT_MARKET" || + orderType == "STOP" || + orderType == "TAKE_PROFIT" { + + orderID, _ := order["orderId"].(float64) + cancelParams := map[string]interface{}{ + "symbol": symbol, + "orderId": int64(orderID), + } + + _, err := t.request("DELETE", "/fapi/v3/order", cancelParams) + if err != nil { + log.Printf(" ⚠ 取消订单 %d 失败: %v", int64(orderID), err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + symbol, int64(orderID), orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + // FormatQuantity 格式化数量(实现Trader接口) func (t *AsterTrader) FormatQuantity(symbol string, quantity float64) (string, error) { formatted, err := t.formatQuantity(symbol, quantity) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 9a68ed17..6e269a5b 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -4,12 +4,14 @@ import ( "encoding/json" "fmt" "log" + "math" "nofx/decision" "nofx/logger" "nofx/market" "nofx/mcp" "nofx/pool" "strings" + "sync" "time" ) @@ -98,10 +100,17 @@ type AutoTrader struct { startTime time.Time // 系统启动时间 callCount int // AI调用次数 positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + stopMonitorCh chan struct{} // 用于停止监控goroutine + monitorWg sync.WaitGroup // 用于等待监控goroutine结束 + peakPnLCache map[string]float64 // 最高收益缓存 (symbol -> 峰值盈亏百分比) + peakPnLCacheMutex sync.RWMutex // 缓存读写锁 + lastBalanceSyncTime time.Time // 上次余额同步时间 + database interface{} // 数据库引用(用于自动更新余额) + userID string // 用户ID } // NewAutoTrader 创建自动交易器 -func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { +func NewAutoTrader(config AutoTraderConfig, database interface{}, userID string) (*AutoTrader, error) { // 设置默认值 if config.ID == "" { config.ID = "default_trader" @@ -195,7 +204,8 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { // 设置默认系统提示词模板 systemPromptTemplate := config.SystemPromptTemplate if systemPromptTemplate == "" { - systemPromptTemplate = "default" // 默认使用 default 模板 + // feature/partial-close-dynamic-tpsl 分支默认使用 adaptive(支持动态止盈止损) + systemPromptTemplate = "adaptive" } return &AutoTrader{ @@ -216,6 +226,13 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { callCount: 0, isRunning: false, positionFirstSeenTime: make(map[string]int64), + stopMonitorCh: make(chan struct{}), + monitorWg: sync.WaitGroup{}, + peakPnLCache: make(map[string]float64), + peakPnLCacheMutex: sync.RWMutex{}, + lastBalanceSyncTime: time.Now(), // 初始化为当前时间 + database: database, + userID: userID, }, nil } @@ -227,6 +244,9 @@ func (at *AutoTrader) Run() error { log.Printf("⚙️ 扫描间隔: %v", at.config.ScanInterval) log.Println("🤖 AI将全权决定杠杆、仓位大小、止损止盈等参数") + // 启动回撤监控 + at.startDrawdownMonitor() + ticker := time.NewTicker(at.config.ScanInterval) defer ticker.Stop() @@ -250,16 +270,113 @@ func (at *AutoTrader) Run() error { // Stop 停止自动交易 func (at *AutoTrader) Stop() { at.isRunning = false + close(at.stopMonitorCh) // 通知监控goroutine停止 + at.monitorWg.Wait() // 等待监控goroutine结束 log.Println("⏹ 自动交易系统停止") } +// autoSyncBalanceIfNeeded 自动同步余额(每10分钟检查一次,变化>5%才更新) +func (at *AutoTrader) autoSyncBalanceIfNeeded() { + // 距离上次同步不足10分钟,跳过 + if time.Since(at.lastBalanceSyncTime) < 10*time.Minute { + return + } + + log.Printf("🔄 [%s] 开始自动检查余额变化...", at.name) + + // 查询实际余额 + balanceInfo, err := at.trader.GetBalance() + if err != nil { + log.Printf("⚠️ [%s] 查询余额失败: %v", at.name, err) + at.lastBalanceSyncTime = time.Now() // 即使失败也更新时间,避免频繁重试 + return + } + + // 提取可用余额 + var actualBalance float64 + if availableBalance, ok := balanceInfo["available_balance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if availableBalance, ok := balanceInfo["availableBalance"].(float64); ok && availableBalance > 0 { + actualBalance = availableBalance + } else if totalBalance, ok := balanceInfo["balance"].(float64); ok && totalBalance > 0 { + actualBalance = totalBalance + } else { + log.Printf("⚠️ [%s] 无法提取可用余额", at.name) + at.lastBalanceSyncTime = time.Now() + return + } + + oldBalance := at.initialBalance + + // 防止除以零:如果初始余额无效,直接更新为实际余额 + if oldBalance <= 0 { + log.Printf("⚠️ [%s] 初始余额无效 (%.2f),直接更新为实际余额 %.2f USDT", at.name, oldBalance, actualBalance) + at.initialBalance = actualBalance + if at.database != nil { + type DatabaseUpdater interface { + UpdateTraderInitialBalance(userID, id string, newBalance float64) error + } + if db, ok := at.database.(DatabaseUpdater); ok { + if err := db.UpdateTraderInitialBalance(at.userID, at.id, actualBalance); err != nil { + log.Printf("❌ [%s] 更新数据库失败: %v", at.name, err) + } else { + log.Printf("✅ [%s] 已自动同步余额到数据库", at.name) + } + } else { + log.Printf("⚠️ [%s] 数据库类型不支持UpdateTraderInitialBalance接口", at.name) + } + } else { + log.Printf("⚠️ [%s] 数据库引用为空,余额仅在内存中更新", at.name) + } + at.lastBalanceSyncTime = time.Now() + return + } + + changePercent := ((actualBalance - oldBalance) / oldBalance) * 100 + + // 变化超过5%才更新 + if math.Abs(changePercent) > 5.0 { + log.Printf("🔔 [%s] 检测到余额大幅变化: %.2f → %.2f USDT (%.2f%%)", + at.name, oldBalance, actualBalance, changePercent) + + // 更新内存中的 initialBalance + at.initialBalance = actualBalance + + // 更新数据库(需要类型断言) + if at.database != nil { + // 这里需要根据实际的数据库类型进行类型断言 + // 由于使用了 interface{},我们需要在 TraderManager 层面处理更新 + // 或者在这里进行类型检查 + type DatabaseUpdater interface { + UpdateTraderInitialBalance(userID, id string, newBalance float64) error + } + if db, ok := at.database.(DatabaseUpdater); ok { + err := db.UpdateTraderInitialBalance(at.userID, at.id, actualBalance) + if err != nil { + log.Printf("❌ [%s] 更新数据库失败: %v", at.name, err) + } else { + log.Printf("✅ [%s] 已自动同步余额到数据库", at.name) + } + } else { + log.Printf("⚠️ [%s] 数据库类型不支持UpdateTraderInitialBalance接口", at.name) + } + } else { + log.Printf("⚠️ [%s] 数据库引用为空,余额仅在内存中更新", at.name) + } + } else { + log.Printf("✓ [%s] 余额变化不大 (%.2f%%),无需更新", at.name, changePercent) + } + + at.lastBalanceSyncTime = time.Now() +} + // runCycle 运行一个交易周期(使用AI全权决策) func (at *AutoTrader) runCycle() error { at.callCount++ - log.Print("\n" + strings.Repeat("=", 70)) + log.Print("\n" + strings.Repeat("=", 70) + "\n") log.Printf("⏰ %s - AI决策周期 #%d", time.Now().Format("2006-01-02 15:04:05"), at.callCount) - log.Print(strings.Repeat("=", 70)) + log.Println(strings.Repeat("=", 70)) // 创建决策记录 record := &logger.DecisionRecord{ @@ -284,7 +401,10 @@ func (at *AutoTrader) runCycle() error { log.Println("📅 日盈亏已重置") } - // 3. 收集交易上下文 + // 3. 自动同步余额(每10分钟检查一次,充值/提现后自动更新) + at.autoSyncBalanceIfNeeded() + + // 4. 收集交易上下文 ctx, err := at.buildTradingContext() if err != nil { record.Success = false @@ -316,7 +436,7 @@ func (at *AutoTrader) runCycle() error { }) } - // 保存候选币种列表 + log.Print(strings.Repeat("=", 70)) for _, coin := range ctx.CandidateCoins { record.CandidateCoins = append(record.CandidateCoins, coin.Symbol) } @@ -324,7 +444,7 @@ func (at *AutoTrader) runCycle() error { log.Printf("📊 账户净值: %.2f USDT | 可用: %.2f USDT | 持仓: %d", ctx.Account.TotalEquity, ctx.Account.AvailableBalance, ctx.Account.PositionCount) - // 4. 调用AI获取完整决策 + // 5. 调用AI获取完整决策 log.Printf("🤖 正在请求AI分析并决策... [模板: %s]", at.systemPromptTemplate) decision, err := decision.GetFullDecisionWithCustomPrompt(ctx, at.mcpClient, at.customPrompt, at.overrideBasePrompt, at.systemPromptTemplate) @@ -345,20 +465,18 @@ func (at *AutoTrader) runCycle() error { // 打印系统提示词和AI思维链(即使有错误,也要输出以便调试) if decision != nil { - if decision.SystemPrompt != "" { - log.Print("\n" + strings.Repeat("=", 70)) + log.Print("\n" + strings.Repeat("=", 70) + "\n") log.Printf("📋 系统提示词 [模板: %s] (错误情况)", at.systemPromptTemplate) log.Println(strings.Repeat("=", 70)) log.Println(decision.SystemPrompt) - log.Print(strings.Repeat("=", 70) + "\n") - } + log.Println(strings.Repeat("=", 70)) if decision.CoTTrace != "" { - log.Print("\n" + strings.Repeat("-", 70)) + log.Print("\n" + strings.Repeat("-", 70) + "\n") log.Println("💭 AI思维链分析(错误情况):") log.Println(strings.Repeat("-", 70)) log.Println(decision.CoTTrace) - log.Print(strings.Repeat("-", 70) + "\n") + log.Println(strings.Repeat("-", 70)) } } @@ -383,13 +501,16 @@ func (at *AutoTrader) runCycle() error { // 7. 打印AI决策 // log.Printf("📋 AI决策列表 (%d 个):\n", len(decision.Decisions)) // for i, d := range decision.Decisions { - // log.Printf(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning) - // if d.Action == "open_long" || d.Action == "open_short" { - // log.Printf(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f", - // d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit) - // } + // log.Printf(" [%d] %s: %s - %s", i+1, d.Symbol, d.Action, d.Reasoning) + // if d.Action == "open_long" || d.Action == "open_short" { + // log.Printf(" 杠杆: %dx | 仓位: %.2f USDT | 止损: %.4f | 止盈: %.4f", + // d.Leverage, d.PositionSizeUSD, d.StopLoss, d.TakeProfit) + // } // } log.Println() + log.Print(strings.Repeat("-", 70)) + // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) + log.Print(strings.Repeat("-", 70)) // 8. 对决策排序:确保先平仓后开仓(防止仓位叠加超限) sortedDecisions := sortDecisionsByPriority(decision.Decisions) @@ -481,6 +602,12 @@ func (at *AutoTrader) buildTradingContext() (*decision.Context, error) { if quantity < 0 { quantity = -quantity // 空仓数量为负,转为正数 } + + // 跳过已平仓的持仓(quantity = 0),防止"幽灵持仓"传递给AI + if quantity == 0 { + continue + } + unrealizedPnl := pos["unRealizedProfit"].(float64) liquidationPrice := pos["liquidationPrice"].(float64) @@ -593,6 +720,12 @@ func (at *AutoTrader) executeDecisionWithRecord(decision *decision.Decision, act return at.executeCloseLongWithRecord(decision, actionRecord) case "close_short": return at.executeCloseShortWithRecord(decision, actionRecord) + case "update_stop_loss": + return at.executeUpdateStopLossWithRecord(decision, actionRecord) + case "update_take_profit": + return at.executeUpdateTakeProfitWithRecord(decision, actionRecord) + case "partial_close": + return at.executePartialCloseWithRecord(decision, actionRecord) case "hold", "wait": // 无需执行,仅记录 return nil @@ -626,6 +759,27 @@ func (at *AutoTrader) executeOpenLongWithRecord(decision *decision.Decision, act actionRecord.Quantity = quantity actionRecord.Price = marketData.CurrentPrice + // ⚠️ 保证金验证:防止保证金不足错误(code=-2019) + requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage) + + balance, err := at.trader.GetBalance() + if err != nil { + return fmt.Errorf("获取账户余额失败: %w", err) + } + availableBalance := 0.0 + if avail, ok := balance["availableBalance"].(float64); ok { + availableBalance = avail + } + + // 手续费估算(Taker费率 0.04%) + estimatedFee := decision.PositionSizeUSD * 0.0004 + totalRequired := requiredMargin + estimatedFee + + if totalRequired > availableBalance { + return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT", + totalRequired, requiredMargin, estimatedFee, availableBalance) + } + // 设置仓位模式 if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil { log.Printf(" ⚠️ 设置仓位模式失败: %v", err) @@ -685,6 +839,27 @@ func (at *AutoTrader) executeOpenShortWithRecord(decision *decision.Decision, ac actionRecord.Quantity = quantity actionRecord.Price = marketData.CurrentPrice + // ⚠️ 保证金验证:防止保证金不足错误(code=-2019) + requiredMargin := decision.PositionSizeUSD / float64(decision.Leverage) + + balance, err := at.trader.GetBalance() + if err != nil { + return fmt.Errorf("获取账户余额失败: %w", err) + } + availableBalance := 0.0 + if avail, ok := balance["availableBalance"].(float64); ok { + availableBalance = avail + } + + // 手续费估算(Taker费率 0.04%) + estimatedFee := decision.PositionSizeUSD * 0.0004 + totalRequired := requiredMargin + estimatedFee + + if totalRequired > availableBalance { + return fmt.Errorf("❌ 保证金不足: 需要 %.2f USDT(保证金 %.2f + 手续费 %.2f),可用 %.2f USDT", + totalRequired, requiredMargin, estimatedFee, availableBalance) + } + // 设置仓位模式 if err := at.trader.SetMarginMode(decision.Symbol, at.config.IsCrossMargin); err != nil { log.Printf(" ⚠️ 设置仓位模式失败: %v", err) @@ -771,6 +946,201 @@ func (at *AutoTrader) executeCloseShortWithRecord(decision *decision.Decision, a return nil } +// executeUpdateStopLossWithRecord 执行调整止损并记录详细信息 +func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error { + log.Printf(" 🎯 调整止损: %s → %.2f", decision.Symbol, decision.NewStopLoss) + + // 获取当前价格 + marketData, err := market.Get(decision.Symbol) + if err != nil { + return err + } + actionRecord.Price = marketData.CurrentPrice + + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + return fmt.Errorf("获取持仓失败: %w", err) + } + + // 查找目标持仓 + var targetPosition map[string]interface{} + for _, pos := range positions { + symbol, _ := pos["symbol"].(string) + posAmt, _ := pos["positionAmt"].(float64) + if symbol == decision.Symbol && posAmt != 0 { + targetPosition = pos + break + } + } + + if targetPosition == nil { + return fmt.Errorf("持仓不存在: %s", decision.Symbol) + } + + // 获取持仓方向和数量 + side, _ := targetPosition["side"].(string) + positionSide := strings.ToUpper(side) + positionAmt, _ := targetPosition["positionAmt"].(float64) + + // 验证新止损价格合理性 + if positionSide == "LONG" && decision.NewStopLoss >= marketData.CurrentPrice { + return fmt.Errorf("多单止损必须低于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss) + } + if positionSide == "SHORT" && decision.NewStopLoss <= marketData.CurrentPrice { + return fmt.Errorf("空单止损必须高于当前价格 (当前: %.2f, 新止损: %.2f)", marketData.CurrentPrice, decision.NewStopLoss) + } + + // 取消旧的止损单(避免多个止损单共存) + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + log.Printf(" ⚠ 取消旧止损单失败: %v", err) + // 不中断执行,继续设置新止损 + } + + // 调用交易所 API 修改止损 + quantity := math.Abs(positionAmt) + err = at.trader.SetStopLoss(decision.Symbol, positionSide, quantity, decision.NewStopLoss) + if err != nil { + return fmt.Errorf("修改止损失败: %w", err) + } + + log.Printf(" ✓ 止损已调整: %.2f (当前价格: %.2f)", decision.NewStopLoss, marketData.CurrentPrice) + return nil +} + +// executeUpdateTakeProfitWithRecord 执行调整止盈并记录详细信息 +func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error { + log.Printf(" 🎯 调整止盈: %s → %.2f", decision.Symbol, decision.NewTakeProfit) + + // 获取当前价格 + marketData, err := market.Get(decision.Symbol) + if err != nil { + return err + } + actionRecord.Price = marketData.CurrentPrice + + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + return fmt.Errorf("获取持仓失败: %w", err) + } + + // 查找目标持仓 + var targetPosition map[string]interface{} + for _, pos := range positions { + symbol, _ := pos["symbol"].(string) + posAmt, _ := pos["positionAmt"].(float64) + if symbol == decision.Symbol && posAmt != 0 { + targetPosition = pos + break + } + } + + if targetPosition == nil { + return fmt.Errorf("持仓不存在: %s", decision.Symbol) + } + + // 获取持仓方向和数量 + side, _ := targetPosition["side"].(string) + positionSide := strings.ToUpper(side) + positionAmt, _ := targetPosition["positionAmt"].(float64) + + // 验证新止盈价格合理性 + if positionSide == "LONG" && decision.NewTakeProfit <= marketData.CurrentPrice { + return fmt.Errorf("多单止盈必须高于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit) + } + if positionSide == "SHORT" && decision.NewTakeProfit >= marketData.CurrentPrice { + return fmt.Errorf("空单止盈必须低于当前价格 (当前: %.2f, 新止盈: %.2f)", marketData.CurrentPrice, decision.NewTakeProfit) + } + + // 取消旧的止盈单(避免多个止盈单共存) + if err := at.trader.CancelStopOrders(decision.Symbol); err != nil { + log.Printf(" ⚠ 取消旧止盈单失败: %v", err) + // 不中断执行,继续设置新止盈 + } + + // 调用交易所 API 修改止盈 + quantity := math.Abs(positionAmt) + err = at.trader.SetTakeProfit(decision.Symbol, positionSide, quantity, decision.NewTakeProfit) + if err != nil { + return fmt.Errorf("修改止盈失败: %w", err) + } + + log.Printf(" ✓ 止盈已调整: %.2f (当前价格: %.2f)", decision.NewTakeProfit, marketData.CurrentPrice) + return nil +} + +// executePartialCloseWithRecord 执行部分平仓并记录详细信息 +func (at *AutoTrader) executePartialCloseWithRecord(decision *decision.Decision, actionRecord *logger.DecisionAction) error { + log.Printf(" 📊 部分平仓: %s %.1f%%", decision.Symbol, decision.ClosePercentage) + + // 验证百分比范围 + if decision.ClosePercentage <= 0 || decision.ClosePercentage > 100 { + return fmt.Errorf("平仓百分比必须在 0-100 之间,当前: %.1f", decision.ClosePercentage) + } + + // 获取当前价格 + marketData, err := market.Get(decision.Symbol) + if err != nil { + return err + } + actionRecord.Price = marketData.CurrentPrice + + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + return fmt.Errorf("获取持仓失败: %w", err) + } + + // 查找目标持仓 + var targetPosition map[string]interface{} + for _, pos := range positions { + symbol, _ := pos["symbol"].(string) + posAmt, _ := pos["positionAmt"].(float64) + if symbol == decision.Symbol && posAmt != 0 { + targetPosition = pos + break + } + } + + if targetPosition == nil { + return fmt.Errorf("持仓不存在: %s", decision.Symbol) + } + + // 获取持仓方向和数量 + side, _ := targetPosition["side"].(string) + positionSide := strings.ToUpper(side) + positionAmt, _ := targetPosition["positionAmt"].(float64) + + // 计算平仓数量 + totalQuantity := math.Abs(positionAmt) + closeQuantity := totalQuantity * (decision.ClosePercentage / 100.0) + actionRecord.Quantity = closeQuantity + + // 执行平仓 + var order map[string]interface{} + if positionSide == "LONG" { + order, err = at.trader.CloseLong(decision.Symbol, closeQuantity) + } else { + order, err = at.trader.CloseShort(decision.Symbol, closeQuantity) + } + + if err != nil { + return fmt.Errorf("部分平仓失败: %w", err) + } + + // 记录订单ID + if orderID, ok := order["orderId"].(int64); ok { + actionRecord.OrderID = orderID + } + + remainingQuantity := totalQuantity - closeQuantity + log.Printf(" ✓ 部分平仓成功: 平仓 %.4f (%.1f%%), 剩余 %.4f", + closeQuantity, decision.ClosePercentage, remainingQuantity) + + return nil +} + // GetID 获取trader ID func (at *AutoTrader) GetID() string { return at.id @@ -984,12 +1354,14 @@ func sortDecisionsByPriority(decisions []decision.Decision) []decision.Decision // 定义优先级 getActionPriority := func(action string) int { switch action { - case "close_long", "close_short": - return 1 // 最高优先级:先平仓 + case "close_long", "close_short", "partial_close": + return 1 // 最高优先级:先平仓(包括部分平仓) + case "update_stop_loss", "update_take_profit": + return 2 // 调整持仓止盈止损 case "open_long", "open_short": - return 2 // 次优先级:后开仓 + return 3 // 次优先级:后开仓 case "hold", "wait": - return 3 // 最低优先级:观望 + return 4 // 最低优先级:观望 default: return 999 // 未知动作放最后 } @@ -1081,3 +1453,158 @@ func normalizeSymbol(symbol string) string { return symbol } + +// 启动回撤监控 +func (at *AutoTrader) startDrawdownMonitor() { + at.monitorWg.Add(1) + go func() { + defer at.monitorWg.Done() + + ticker := time.NewTicker(1 * time.Minute) // 每分钟检查一次 + defer ticker.Stop() + + log.Println("📊 启动持仓回撤监控(每分钟检查一次)") + + for { + select { + case <-ticker.C: + at.checkPositionDrawdown() + case <-at.stopMonitorCh: + log.Println("⏹ 停止持仓回撤监控") + return + } + } + }() +} + +// 检查持仓回撤情况 +func (at *AutoTrader) checkPositionDrawdown() { + // 获取当前持仓 + positions, err := at.trader.GetPositions() + if err != nil { + log.Printf("❌ 回撤监控:获取持仓失败: %v", err) + return + } + + for _, pos := range positions { + symbol := pos["symbol"].(string) + side := pos["side"].(string) + entryPrice := pos["entryPrice"].(float64) + markPrice := pos["markPrice"].(float64) + quantity := pos["positionAmt"].(float64) + if quantity < 0 { + quantity = -quantity // 空仓数量为负,转为正数 + } + + // 计算当前盈亏百分比 + leverage := 10 // 默认值 + if lev, ok := pos["leverage"].(float64); ok { + leverage = int(lev) + } + + var currentPnLPct float64 + if side == "long" { + currentPnLPct = ((markPrice - entryPrice) / entryPrice) * float64(leverage) * 100 + } else { + currentPnLPct = ((entryPrice - markPrice) / entryPrice) * float64(leverage) * 100 + } + + // 获取该持仓的历史最高收益 + at.peakPnLCacheMutex.RLock() + peakPnLPct, exists := at.peakPnLCache[symbol] + at.peakPnLCacheMutex.RUnlock() + + if !exists { + // 如果没有历史最高记录,使用当前盈亏作为初始值 + peakPnLPct = currentPnLPct + at.UpdatePeakPnL(symbol, currentPnLPct) + } else { + // 更新峰值缓存 + at.UpdatePeakPnL(symbol, currentPnLPct) + } + + // 计算回撤(从最高点下跌的幅度) + var drawdownPct float64 + if peakPnLPct > 0 && currentPnLPct < peakPnLPct { + drawdownPct = ((peakPnLPct - currentPnLPct) / peakPnLPct) * 100 + } + + // 检查平仓条件:收益大于5%且回撤超过40% + if currentPnLPct > 5.0 && drawdownPct >= 40.0 { + log.Printf("🚨 触发回撤平仓条件: %s %s | 当前收益: %.2f%% | 最高收益: %.2f%% | 回撤: %.2f%%", + symbol, side, currentPnLPct, peakPnLPct, drawdownPct) + + // 执行平仓 + if err := at.emergencyClosePosition(symbol, side); err != nil { + log.Printf("❌ 回撤平仓失败 (%s %s): %v", symbol, side, err) + } else { + log.Printf("✅ 回撤平仓成功: %s %s", symbol, side) + // 平仓后清理该symbol的缓存 + at.ClearPeakPnLCache(symbol) + } + } else if currentPnLPct > 5.0 { + // 记录接近平仓条件的情况(用于调试) + log.Printf("📊 回撤监控: %s %s | 收益: %.2f%% | 最高: %.2f%% | 回撤: %.2f%%", + symbol, side, currentPnLPct, peakPnLPct, drawdownPct) + } + } +} + +// 紧急平仓函数 +func (at *AutoTrader) emergencyClosePosition(symbol, side string) error { + switch side { + case "long": + order, err := at.trader.CloseLong(symbol, 0) // 0 = 全部平仓 + if err != nil { + return err + } + log.Printf("✅ 紧急平多仓成功,订单ID: %v", order["orderId"]) + case "short": + order, err := at.trader.CloseShort(symbol, 0) // 0 = 全部平仓 + if err != nil { + return err + } + log.Printf("✅ 紧急平空仓成功,订单ID: %v", order["orderId"]) + default: + return fmt.Errorf("未知的持仓方向: %s", side) + } + + return nil +} + +// GetPeakPnLCache 获取最高收益缓存 +func (at *AutoTrader) GetPeakPnLCache() map[string]float64 { + at.peakPnLCacheMutex.RLock() + defer at.peakPnLCacheMutex.RUnlock() + + // 返回缓存的副本 + cache := make(map[string]float64) + for k, v := range at.peakPnLCache { + cache[k] = v + } + return cache +} + +// UpdatePeakPnL 更新最高收益缓存 +func (at *AutoTrader) UpdatePeakPnL(symbol string, currentPnLPct float64) { + at.peakPnLCacheMutex.Lock() + defer at.peakPnLCacheMutex.Unlock() + + if peak, exists := at.peakPnLCache[symbol]; exists { + // 更新峰值(如果是多头,取较大值;如果是空头,currentPnLPct为负,也要比较) + if currentPnLPct > peak { + at.peakPnLCache[symbol] = currentPnLPct + } + } else { + // 首次记录 + at.peakPnLCache[symbol] = currentPnLPct + } +} + +// ClearPeakPnLCache 清除指定symbol的峰值缓存 +func (at *AutoTrader) ClearPeakPnLCache(symbol string) { + at.peakPnLCacheMutex.Lock() + defer at.peakPnLCacheMutex.Unlock() + + delete(at.peakPnLCache, symbol) +} diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..e8e8f083 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "strconv" + "strings" "sync" "time" @@ -32,10 +33,56 @@ type FuturesTrader struct { // NewFuturesTrader 创建合约交易器 func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader { client := futures.NewClient(apiKey, secretKey) - return &FuturesTrader{ + // 同步时间,避免 Timestamp ahead 错误 + syncBinanceServerTime(client) + trader := &FuturesTrader{ client: client, cacheDuration: 15 * time.Second, // 15秒缓存 } + + // 设置双向持仓模式(Hedge Mode) + // 这是必需的,因为代码中使用了 PositionSide (LONG/SHORT) + if err := trader.setDualSidePosition(); err != nil { + log.Printf("⚠️ 设置双向持仓模式失败: %v (如果已是双向模式则忽略此警告)", err) + } + + return trader +} + +// setDualSidePosition 设置双向持仓模式(初始化时调用) +func (t *FuturesTrader) setDualSidePosition() error { + // 尝试设置双向持仓模式 + err := t.client.NewChangePositionModeService(). + DualSide(true). // true = 双向持仓(Hedge Mode) + Do(context.Background()) + + if err != nil { + // 如果错误信息包含"No need to change",说明已经是双向持仓模式 + if strings.Contains(err.Error(), "No need to change position side") { + log.Printf(" ✓ 账户已是双向持仓模式(Hedge Mode)") + return nil + } + // 其他错误则返回(但在调用方不会中断初始化) + return err + } + + log.Printf(" ✓ 账户已切换为双向持仓模式(Hedge Mode)") + log.Printf(" ℹ️ 双向持仓模式允许同时持有多单和空单") + return nil +} + +// syncBinanceServerTime 同步币安服务器时间,确保请求时间戳合法 +func syncBinanceServerTime(client *futures.Client) { + serverTime, err := client.NewServerTimeService().Do(context.Background()) + if err != nil { + log.Printf("⚠️ 同步币安服务器时间失败: %v", err) + return + } + + now := time.Now().UnixMilli() + offset := now - serverTime + client.TimeOffset = offset + log.Printf("⏱ 已同步币安服务器时间,偏移 %dms", offset) } // GetBalance 获取账户余额(带缓存) @@ -162,6 +209,17 @@ func (t *FuturesTrader) SetMarginMode(symbol string, isCrossMargin bool) error { log.Printf(" ⚠️ %s 有持仓,无法更改仓位模式,继续使用当前模式", symbol) return nil } + // 检测多资产模式(错误码 -4168) + if contains(err.Error(), "Multi-Assets mode") || contains(err.Error(), "-4168") || contains(err.Error(), "4168") { + log.Printf(" ⚠️ %s 检测到多资产模式,强制使用全仓模式", symbol) + log.Printf(" 💡 提示:如需使用逐仓模式,请在币安关闭多资产模式") + return nil + } + // 检测统一账户 API(Portfolio Margin) + if contains(err.Error(), "unified") || contains(err.Error(), "portfolio") || contains(err.Error(), "Portfolio") { + log.Printf(" ❌ %s 检测到统一账户 API,无法进行合约交易", symbol) + return fmt.Errorf("请使用「现货与合约交易」API 权限,不要使用「统一账户 API」") + } log.Printf(" ⚠️ 设置仓位模式失败: %v", err) // 不返回错误,让交易继续 return nil @@ -237,6 +295,17 @@ func (t *FuturesTrader) OpenLong(symbol string, quantity float64, leverage int) return nil, err } + // ✅ 检查格式化后的数量是否为 0(防止四舍五入导致的错误) + quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64) + if parseErr != nil || quantityFloat <= 0 { + return nil, fmt.Errorf("开倉數量過小,格式化後為 0 (原始: %.8f → 格式化: %s)。建議增加開倉金額或選擇價格更低的幣種", quantity, quantityStr) + } + + // ✅ 检查最小名义价值(Binance 要求至少 10 USDT) + if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { + return nil, err + } + // 创建市价买入订单 order, err := t.client.NewCreateOrderService(). Symbol(symbol). @@ -280,6 +349,17 @@ func (t *FuturesTrader) OpenShort(symbol string, quantity float64, leverage int) return nil, err } + // ✅ 检查格式化后的数量是否为 0(防止四舍五入导致的错误) + quantityFloat, parseErr := strconv.ParseFloat(quantityStr, 64) + if parseErr != nil || quantityFloat <= 0 { + return nil, fmt.Errorf("开倉數量過小,格式化後為 0 (原始: %.8f → 格式化: %s)。建議增加開倉金額或選擇價格更低的幣種", quantity, quantityStr) + } + + // ✅ 检查最小名义价值(Binance 要求至少 10 USDT) + if err := t.CheckMinNotional(symbol, quantityFloat); err != nil { + return nil, err + } + // 创建市价卖出订单 order, err := t.client.NewCreateOrderService(). Symbol(symbol). @@ -411,6 +491,92 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] return result, nil } + + +// CancelStopLossOrders 仅取消止损单(不影响止盈单) +func (t *FuturesTrader) CancelStopLossOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止损订单(不取消止盈订单) + if orderType == futures.OrderTypeStopMarket || orderType == futures.OrderTypeStop { + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消止损单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止损单 (订单ID: %d, 类型: %s)", order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止损单", symbol, canceledCount) + } + + return nil +} + +// CancelTakeProfitOrders 仅取消止盈单(不影响止损单) +func (t *FuturesTrader) CancelTakeProfitOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止盈单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止盈订单(不取消止损订单) + if orderType == futures.OrderTypeTakeProfitMarket || orderType == futures.OrderTypeTakeProfit { + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消止盈单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消止盈单 (订单ID: %d, 类型: %s)", order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈单", symbol, canceledCount) + } + + return nil +} + // CancelAllOrders 取消该币种的所有挂单 func (t *FuturesTrader) CancelAllOrders(symbol string) error { err := t.client.NewCancelAllOpenOrdersService(). @@ -425,6 +591,53 @@ func (t *FuturesTrader) CancelAllOrders(symbol string) error { return nil } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *FuturesTrader) CancelStopOrders(symbol string) error { + // 获取该币种的所有未完成订单 + orders, err := t.client.NewListOpenOrdersService(). + Symbol(symbol). + Do(context.Background()) + + if err != nil { + return fmt.Errorf("获取未完成订单失败: %w", err) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range orders { + orderType := order.Type + + // 只取消止损和止盈订单 + if orderType == futures.OrderTypeStopMarket || + orderType == futures.OrderTypeTakeProfitMarket || + orderType == futures.OrderTypeStop || + orderType == futures.OrderTypeTakeProfit { + + _, err := t.client.NewCancelOrderService(). + Symbol(symbol). + OrderID(order.OrderID). + Do(context.Background()) + + if err != nil { + log.Printf(" ⚠ 取消订单 %d 失败: %v", order.OrderID, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 类型: %s)", + symbol, order.OrderID, orderType) + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有止盈/止损单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + } + + return nil +} + // GetMarketPrice 获取市场价格 func (t *FuturesTrader) GetMarketPrice(symbol string) (float64, error) { prices, err := t.client.NewListPricesService().Symbol(symbol).Do(context.Background()) @@ -528,6 +741,32 @@ func (t *FuturesTrader) SetTakeProfit(symbol string, positionSide string, quanti return nil } +// GetMinNotional 获取最小名义价值(Binance要求) +func (t *FuturesTrader) GetMinNotional(symbol string) float64 { + // 使用保守的默认值 10 USDT,确保订单能够通过交易所验证 + return 10.0 +} + +// CheckMinNotional 检查订单是否满足最小名义价值要求 +func (t *FuturesTrader) CheckMinNotional(symbol string, quantity float64) error { + price, err := t.GetMarketPrice(symbol) + if err != nil { + return fmt.Errorf("获取市价失败: %w", err) + } + + notionalValue := quantity * price + minNotional := t.GetMinNotional(symbol) + + if notionalValue < minNotional { + return fmt.Errorf( + "订单金额 %.2f USDT 低于最小要求 %.2f USDT (数量: %.4f, 价格: %.4f)", + notionalValue, minNotional, quantity, price, + ) + } + + return nil +} + // GetSymbolPrecision 获取交易对的数量精度 func (t *FuturesTrader) GetSymbolPrecision(symbol string) (int, error) { exchangeInfo, err := t.client.NewExchangeInfoService().Do(context.Background()) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..078b2135 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,10 +2,12 @@ package trader import ( "context" + "crypto/ecdsa" "encoding/json" "fmt" "log" "strconv" + "strings" "github.com/ethereum/go-ethereum/crypto" "github.com/sonirico/go-hyperliquid" @@ -22,6 +24,9 @@ type HyperliquidTrader struct { // NewHyperliquidTrader 创建Hyperliquid交易器 func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) (*HyperliquidTrader, error) { + // 去掉私钥的 0x 前缀(如果有,不区分大小写) + privateKeyHex = strings.TrimPrefix(strings.ToLower(privateKeyHex), "0x") + // 解析私钥 privateKey, err := crypto.HexToECDSA(privateKeyHex) if err != nil { @@ -34,13 +39,18 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) apiURL = hyperliquid.TestnetAPIURL } - // // 从私钥生成钱包地址 - // pubKey := privateKey.Public() - // publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) - // if !ok { - // return nil, fmt.Errorf("无法转换公钥") - // } - // walletAddr := crypto.PubkeyToAddress(*publicKeyECDSA).Hex() + // 从私钥生成钱包地址(如果未提供) + if walletAddr == "" { + pubKey := privateKey.Public() + publicKeyECDSA, ok := pubKey.(*ecdsa.PublicKey) + if !ok { + return nil, fmt.Errorf("无法转换公钥") + } + walletAddr = crypto.PubkeyToAddress(*publicKeyECDSA).Hex() + log.Printf("✓ 从私钥自动生成钱包地址: %s", walletAddr) + } else { + log.Printf("✓ 使用提供的钱包地址: %s", walletAddr) + } ctx := context.Background() @@ -76,23 +86,54 @@ func NewHyperliquidTrader(privateKeyHex string, walletAddr string, testnet bool) func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { log.Printf("🔄 正在调用Hyperliquid API获取账户余额...") - // 获取账户状态 + // ✅ Step 1: 查询 Spot 现货账户余额 + spotState, err := t.exchange.Info().SpotUserState(t.ctx, t.walletAddr) + var spotUSDCBalance float64 = 0.0 + if err != nil { + log.Printf("⚠️ 查询 Spot 余额失败(可能无现货资产): %v", err) + } else if spotState != nil && len(spotState.Balances) > 0 { + for _, balance := range spotState.Balances { + if balance.Coin == "USDC" { + spotUSDCBalance, _ = strconv.ParseFloat(balance.Total, 64) + log.Printf("✓ 发现 Spot 现货余额: %.2f USDC", spotUSDCBalance) + break + } + } + } + + // ✅ Step 2: 查询 Perpetuals 合约账户状态 accountState, err := t.exchange.Info().UserState(t.ctx, t.walletAddr) if err != nil { - log.Printf("❌ Hyperliquid API调用失败: %v", err) + log.Printf("❌ Hyperliquid Perpetuals API调用失败: %v", err) return nil, fmt.Errorf("获取账户信息失败: %w", err) } // 解析余额信息(MarginSummary字段都是string) result := make(map[string]interface{}) - // 🔍 调试:打印API返回的完整CrossMarginSummary结构 - summaryJSON, _ := json.MarshalIndent(accountState.MarginSummary, " ", " ") - log.Printf("🔍 [DEBUG] Hyperliquid API CrossMarginSummary完整数据:") - log.Printf("%s", string(summaryJSON)) + // ✅ Step 3: 根据保证金模式动态选择正确的摘要(CrossMarginSummary 或 MarginSummary) + var accountValue, totalMarginUsed float64 + var summaryType string + var summary interface{} - accountValue, _ := strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) - totalMarginUsed, _ := strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) + if t.isCrossMargin { + // 全仓模式:使用 CrossMarginSummary + accountValue, _ = strconv.ParseFloat(accountState.CrossMarginSummary.AccountValue, 64) + totalMarginUsed, _ = strconv.ParseFloat(accountState.CrossMarginSummary.TotalMarginUsed, 64) + summaryType = "CrossMarginSummary (全仓)" + summary = accountState.CrossMarginSummary + } else { + // 逐仓模式:使用 MarginSummary + accountValue, _ = strconv.ParseFloat(accountState.MarginSummary.AccountValue, 64) + totalMarginUsed, _ = strconv.ParseFloat(accountState.MarginSummary.TotalMarginUsed, 64) + summaryType = "MarginSummary (逐仓)" + summary = accountState.MarginSummary + } + + // 🔍 调试:打印API返回的完整摘要结构 + summaryJSON, _ := json.MarshalIndent(summary, " ", " ") + log.Printf("🔍 [DEBUG] Hyperliquid API %s 完整数据:", summaryType) + log.Printf("%s", string(summaryJSON)) // ⚠️ 关键修复:从所有持仓中累加真正的未实现盈亏 totalUnrealizedPnl := 0.0 @@ -109,16 +150,47 @@ func (t *HyperliquidTrader) GetBalance() (map[string]interface{}, error) { // 需要返回"不包含未实现盈亏的钱包余额" walletBalanceWithoutUnrealized := accountValue - totalUnrealizedPnl - result["totalWalletBalance"] = walletBalanceWithoutUnrealized // 钱包余额(不含未实现盈亏) - result["availableBalance"] = accountValue - totalMarginUsed // 可用余额(总净值 - 占用保证金) - result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未实现盈亏 + // ✅ Step 4: 使用 Withdrawable 欄位(PR #443) + // Withdrawable 是官方提供的真实可提现余额,比简单计算更可靠 + availableBalance := 0.0 + if accountState.Withdrawable != "" { + withdrawable, err := strconv.ParseFloat(accountState.Withdrawable, 64) + if err == nil && withdrawable > 0 { + availableBalance = withdrawable + log.Printf("✓ 使用 Withdrawable 作为可用余额: %.2f", availableBalance) + } + } - log.Printf("✓ Hyperliquid 账户: 总净值=%.2f (钱包%.2f+未实现%.2f), 可用=%.2f, 保证金占用=%.2f", + // 降级方案:如果没有 Withdrawable,使用简单计算 + if availableBalance == 0 && accountState.Withdrawable == "" { + availableBalance = accountValue - totalMarginUsed + if availableBalance < 0 { + log.Printf("⚠️ 计算出的可用余额为负数 (%.2f),重置为 0", availableBalance) + availableBalance = 0 + } + } + + // ✅ Step 5: 正確處理 Spot + Perpetuals 余额 + // 重要:Spot 只加到總資產,不加到可用餘額 + // 原因:Spot 和 Perpetuals 是獨立帳戶,需手動 ClassTransfer 才能轉帳 + totalWalletBalance := walletBalanceWithoutUnrealized + spotUSDCBalance + + result["totalWalletBalance"] = totalWalletBalance // 總資產(Perp + Spot) + result["availableBalance"] = availableBalance // 可用餘額(僅 Perpetuals,不含 Spot) + result["totalUnrealizedProfit"] = totalUnrealizedPnl // 未實現盈虧(僅來自 Perpetuals) + result["spotBalance"] = spotUSDCBalance // Spot 現貨餘額(單獨返回) + + log.Printf("✓ Hyperliquid 完整账户:") + log.Printf(" • Spot 现货余额: %.2f USDC (需手动转账到 Perpetuals 才能开仓)", spotUSDCBalance) + log.Printf(" • Perpetuals 合约净值: %.2f USDC (钱包%.2f + 未实现%.2f)", accountValue, walletBalanceWithoutUnrealized, - totalUnrealizedPnl, - result["availableBalance"], - totalMarginUsed) + totalUnrealizedPnl) + log.Printf(" • Perpetuals 可用余额: %.2f USDC (可直接用於開倉)", availableBalance) + log.Printf(" • 保证金占用: %.2f USDC", totalMarginUsed) + log.Printf(" • 總資產 (Perp+Spot): %.2f USDC", totalWalletBalance) + log.Printf(" ⭐ 总资产: %.2f USDC | Perp 可用: %.2f USDC | Spot 余额: %.2f USDC", + totalWalletBalance, availableBalance, spotUSDCBalance) return result, nil } @@ -477,6 +549,25 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str return result, nil } +// CancelStopOrders 取消该币种的止盈/止 + + +// CancelStopLossOrders 仅取消止损单(Hyperliquid 暂无法区分止损和止盈,取消所有) +func (t *HyperliquidTrader) CancelStopLossOrders(symbol string) error { + // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 无法区分止损和止盈单,因此取消该币种的所有挂单 + log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单") + return t.CancelStopOrders(symbol) +} + +// CancelTakeProfitOrders 仅取消止盈单(Hyperliquid 暂无法区分止损和止盈,取消所有) +func (t *HyperliquidTrader) CancelTakeProfitOrders(symbol string) error { + // Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 无法区分止损和止盈单,因此取消该币种的所有挂单 + log.Printf(" ⚠️ Hyperliquid 无法区分止损/止盈单,将取消所有挂单") + return t.CancelStopOrders(symbol) +} + // CancelAllOrders 取消该币种的所有挂单 func (t *HyperliquidTrader) CancelAllOrders(symbol string) error { coin := convertSymbolToHyperliquid(symbol) @@ -501,6 +592,40 @@ func (t *HyperliquidTrader) CancelAllOrders(symbol string) error { return nil } +// CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) +func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { + coin := convertSymbolToHyperliquid(symbol) + + // 获取所有挂单 + openOrders, err := t.exchange.Info().OpenOrders(t.ctx, t.walletAddr) + if err != nil { + return fmt.Errorf("获取挂单失败: %w", err) + } + + // 注意:Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 因此暂时取消该币种的所有挂单(包括止盈止损单) + // 这是安全的,因为在设置新的止盈止损之前,应该清理所有旧订单 + canceledCount := 0 + for _, order := range openOrders { + if order.Coin == coin { + _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) + if err != nil { + log.Printf(" ⚠ 取消订单失败 (oid=%d): %v", order.Oid, err) + continue + } + canceledCount++ + } + } + + if canceledCount == 0 { + log.Printf(" ℹ %s 没有挂单需要取消", symbol) + } else { + log.Printf(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount) + } + + return nil +} + // GetMarketPrice 获取市场价格 func (t *HyperliquidTrader) GetMarketPrice(symbol string) (float64, error) { coin := convertSymbolToHyperliquid(symbol) diff --git a/trader/interface.go b/trader/interface.go index 18d75ee7..3d3a6e90 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -36,9 +36,18 @@ type Trader interface { // SetTakeProfit 设置止盈单 SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error + // CancelStopLossOrders 仅取消止损单(修复 BUG:调整止损时不删除止盈) + CancelStopLossOrders(symbol string) error + + // CancelTakeProfitOrders 仅取消止盈单(修复 BUG:调整止盈时不删除止损) + CancelTakeProfitOrders(symbol string) error + // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error + // CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) + CancelStopOrders(symbol string) error + // FormatQuantity 格式化数量到正确的精度 FormatQuantity(symbol string, quantity float64) (string, error) } diff --git a/view_pg_data.sh b/view_pg_data.sh index 1ae89206..59a7aef5 100755 --- a/view_pg_data.sh +++ b/view_pg_data.sh @@ -59,15 +59,7 @@ GROUP BY used ORDER BY used; " -echo -e "\n📝 未使用的内测码:" -$DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c " -SELECT code -FROM beta_codes -WHERE used = false -ORDER BY created_at DESC; -" - echo -e "\n👥 用户信息:" $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c " SELECT id, email, otp_verified, created_at FROM users ORDER BY created_at; -" +" \ No newline at end of file diff --git a/web/.husky/pre-commit b/web/.husky/pre-commit new file mode 100644 index 00000000..72c4429b --- /dev/null +++ b/web/.husky/pre-commit @@ -0,0 +1 @@ +npm test diff --git a/web/.prettierignore b/web/.prettierignore new file mode 100644 index 00000000..2ca5a7fe --- /dev/null +++ b/web/.prettierignore @@ -0,0 +1,22 @@ +# Dependencies +node_modules + +# Build outputs +dist +build +*.tsbuildinfo + +# Config files +pnpm-lock.yaml +package-lock.json +yarn.lock + +# Logs +*.log + +# Coverage +coverage + +# IDE +.vscode +.idea diff --git a/web/.prettierrc.json b/web/.prettierrc.json new file mode 100644 index 00000000..6cd408d1 --- /dev/null +++ b/web/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "semi": false, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf", + "arrowParens": "always", + "bracketSpacing": true, + "jsxSingleQuote": false, + "quoteProps": "as-needed" +} diff --git a/web/eslint.config.js b/web/eslint.config.js new file mode 100644 index 00000000..625ec65b --- /dev/null +++ b/web/eslint.config.js @@ -0,0 +1,89 @@ +import js from '@eslint/js' +import tseslint from '@typescript-eslint/eslint-plugin' +import tsparser from '@typescript-eslint/parser' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import prettier from 'eslint-plugin-prettier' + +export default [ + { + ignores: ['dist', 'node_modules', 'build', '*.config.js'] + }, + js.configs.recommended, + { + files: ['**/*.{ts,tsx}'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true + } + }, + globals: { + window: 'readonly', + document: 'readonly', + console: 'readonly', + setTimeout: 'readonly', + clearTimeout: 'readonly', + setInterval: 'readonly', + clearInterval: 'readonly', + fetch: 'readonly', + localStorage: 'readonly', + sessionStorage: 'readonly' + } + }, + plugins: { + '@typescript-eslint': tseslint, + 'react': react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + 'prettier': prettier + }, + rules: { + ...tseslint.configs.recommended.rules, + ...react.configs.recommended.rules, + ...reactHooks.configs.recommended.rules, + + // Prettier integration + 'prettier/prettier': 'error', + + // React rules + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + // 该规则在 TS 项目中经常与 TS 的类型检查重复,关闭以避免误报 + 'no-undef': 'off', + + // TypeScript rules + // 放宽以下规则以避免在不改变功能的情况下大面积改动代码 + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-unused-vars': 'off', + + // React Refresh + 'react-refresh/only-export-components': 'off', + + // General rules + 'no-console': 'off', + 'no-debugger': 'off', + + // 新版 react-hooks 推荐规则在本项目会造成大量误报,关闭以免影响开发体验 + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/static-components': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + + // 某些字符串中包含未转义字符用于展示,关闭以避免不必要的修改 + 'react/no-unescaped-entities': 'off', + + // 可视情况关闭依赖数组校验(如需严格可改为 'warn') + 'react-hooks/exhaustive-deps': 'off' + }, + settings: { + react: { + version: 'detect' + } + } + } +] diff --git a/web/package-lock.json b/web/package-lock.json index 08b930ea..a117b2cb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -22,11 +22,23 @@ "zustand": "^5.0.2" }, "devDependencies": { + "@eslint/js": "^9.39.1", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", "postcss": "^8.4.49", + "prettier": "^3.6.2", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "vite": "^6.0.7" @@ -732,6 +744,247 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -839,6 +1092,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1265,6 +1531,13 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -1291,6 +1564,255 @@ "@types/react": "^18.0.0" } }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.3.tgz", + "integrity": "sha512-sbaQ27XBUopBkRiuY/P9sWGOWUW4rl8fDoHIUmLpZd8uldsTyB4/Zg6bWTegPoTLnKj9Hqgn3QD6cjPNB32Odw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/type-utils": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.46.3", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.46.3.tgz", + "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.46.3.tgz", + "integrity": "sha512-Fz8yFXsp2wDFeUElO88S9n4w1I4CWDTXDqDr9gYvZgUpwXQqmZBr9+NTTql5R3J7+hrJZPdpiWaB9VNhAKYLuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.46.3", + "@typescript-eslint/types": "^8.46.3", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.3.tgz", + "integrity": "sha512-FCi7Y1zgrmxp3DfWfr+3m9ansUUFoy8dkEdeQSgA9gbm8DaHYvZCdkFRQrtKiedFf3Ha6VmoqoAaP68+i+22kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.3.tgz", + "integrity": "sha512-GLupljMniHNIROP0zE7nCcybptolcH8QZfXOpCfhQDAdwJ/ZTlcaBOYebSOZotpti/3HrHSw7D3PZm75gYFsOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.46.3.tgz", + "integrity": "sha512-ZPCADbr+qfz3aiTTYNNkCbUt+cjNwI/5McyANNrFBpVxPt7GqpEYz5ZfdwuFyGUnJ9FdDXbGODUu6iRCI6XRXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3", + "@typescript-eslint/utils": "8.46.3", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.46.3.tgz", + "integrity": "sha512-G7Ok9WN/ggW7e/tOf8TQYMaxgID3Iujn231hfi0Pc7ZheztIJVpO44ekY00b7akqc6nZcvregk0Jpah3kep6hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.3.tgz", + "integrity": "sha512-f/NvtRjOm80BtNM5OQtlaBdM5BRFUv7gf381j9wygDNL+qOYSNOgtQ/DCndiYi80iIOv76QqaTmp4fa9hwI0OA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.46.3", + "@typescript-eslint/tsconfig-utils": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/visitor-keys": "8.46.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.46.3.tgz", + "integrity": "sha512-VXw7qmdkucEx9WkmR3ld/u6VhRyKeiF1uxWwCy/iuNfokjJ7VhsgLSOTjsol8BunSw190zABzpwdNsze2Kpo4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.3", + "@typescript-eslint/types": "8.46.3", + "@typescript-eslint/typescript-estree": "8.46.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.46.3", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.3.tgz", + "integrity": "sha512-uk574k8IU0rOF/AjniX8qbLSGURJVUCeM5e4MIMKBFFi8weeiLrG1fyQejyLXQpRZbU/1BuQasleV/RfHC3hHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.46.3", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", @@ -1311,6 +1833,63 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.2.0.tgz", + "integrity": "sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -1360,6 +1939,161 @@ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", "dev": true }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -1397,6 +2131,22 @@ "postcss": "^8.1.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1479,6 +2229,66 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1508,6 +2318,39 @@ } ] }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -1556,6 +2399,56 @@ "url": "https://polar.sh/cva" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/cli-truncate/-/cli-truncate-5.1.1.tgz", + "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^7.1.0", + "string-width": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "8.1.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-8.1.0.tgz", + "integrity": "sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -1582,6 +2475,13 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -1591,6 +2491,13 @@ "node": ">= 6" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -1738,6 +2645,60 @@ "node": ">=12" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -1769,6 +2730,49 @@ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -1789,6 +2793,19 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -1798,6 +2815,21 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -1816,6 +2848,196 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", @@ -1866,11 +3088,406 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", + "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-react/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-react/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-equals": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.2.tgz", @@ -1907,6 +3524,20 @@ "node": ">= 6" } }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -1916,6 +3547,19 @@ "reusify": "^1.0.4" } }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1928,6 +3572,60 @@ "node": ">=8" } }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2007,6 +3705,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2016,6 +3755,76 @@ "node": ">=6.9.0" } }, + "node_modules/get-east-asian-width": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", + "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -2048,6 +3857,137 @@ "node": ">=10.13.0" } }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2060,6 +4000,91 @@ "node": ">= 0.4" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -2068,6 +4093,60 @@ "node": ">=12" } }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -2080,6 +4159,36 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -2095,6 +4204,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2104,6 +4248,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -2113,6 +4273,26 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2125,6 +4305,32 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2134,12 +4340,199 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmmirror.com/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -2170,6 +4563,19 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -2182,6 +4588,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2194,6 +4621,46 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmmirror.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2212,11 +4679,200 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lint-staged": { + "version": "16.2.6", + "resolved": "https://registry.npmmirror.com/lint-staged/-/lint-staged-16.2.6.tgz", + "integrity": "sha512-s1gphtDbV4bmW1eylXpVMk2u7is7YsrLl8hzrtvC70h4ByhcMLZFY01Fx05ZUDNuv1H8HO4E+e2zgejV1jVwNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^14.0.1", + "listr2": "^9.0.5", + "micromatch": "^4.0.8", + "nano-spawn": "^2.0.0", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.8.1" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/listr2": { + "version": "9.0.5", + "resolved": "https://registry.npmmirror.com/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^5.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmmirror.com/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -2246,6 +4902,16 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2268,6 +4934,19 @@ "node": ">=8.6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -2324,6 +5003,19 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nano-spawn": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/nano-spawn/-/nano-spawn-2.0.0.tgz", + "integrity": "sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/nano-spawn?sponsor=1" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -2342,6 +5034,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -2383,12 +5082,217 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2444,6 +5348,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -2462,6 +5379,16 @@ "node": ">= 6" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -2619,6 +5546,46 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -2634,6 +5601,16 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2773,6 +5750,50 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve": { "version": "1.22.11", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", @@ -2793,6 +5814,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2803,6 +5851,13 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, "node_modules/rollup": { "version": "4.52.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", @@ -2867,6 +5922,61 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -2884,6 +5994,55 @@ "semver": "bin/semver.js" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2905,6 +6064,82 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -2917,6 +6152,39 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmmirror.com/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2926,6 +6194,30 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmmirror.com/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", @@ -2985,6 +6277,104 @@ "node": ">=8" } }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmmirror.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -3022,6 +6412,19 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -3044,6 +6447,19 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -3068,6 +6484,22 @@ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -3199,6 +6631,19 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -3211,11 +6656,103 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3224,6 +6761,25 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -3254,6 +6810,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -3409,6 +6975,105 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -3506,6 +7171,56 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmmirror.com/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", diff --git a/web/package.json b/web/package.json index ed1c0732..f8c4655a 100644 --- a/web/package.json +++ b/web/package.json @@ -5,7 +5,12 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint:fix": "eslint . --ext ts,tsx --fix", + "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", + "prepare": "husky" }, "dependencies": { "@radix-ui/react-slot": "^1.2.3", @@ -22,13 +27,34 @@ "zustand": "^5.0.2" }, "devDependencies": { + "@eslint/js": "^9.39.1", "@types/react": "^18.3.17", "@types/react-dom": "^18.3.5", + "@typescript-eslint/eslint-plugin": "^8.46.3", + "@typescript-eslint/parser": "^8.46.3", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-react": "^7.37.5", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "husky": "^9.1.7", + "lint-staged": "^16.2.6", "postcss": "^8.4.49", + "prettier": "^3.6.2", "tailwindcss": "^3.4.17", "typescript": "^5.8.3", "vite": "^6.0.7" + }, + "lint-staged": { + "*.{ts,tsx}": [ + "eslint --fix", + "prettier --write" + ], + "*.{css,json}": [ + "prettier --write" + ] } } diff --git a/web/public/images/guide.png b/web/public/images/guide.png new file mode 100644 index 00000000..cd1abd2e Binary files /dev/null and b/web/public/images/guide.png differ diff --git a/web/src/App.tsx b/web/src/App.tsx index 14cf29d6..b57081c5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,18 +1,19 @@ -import { useEffect, useState } from 'react'; -import useSWR from 'swr'; -import { api } from './lib/api'; -import { EquityChart } from './components/EquityChart'; -import { AITradersPage } from './components/AITradersPage'; -import { LoginPage } from './components/LoginPage'; -import { RegisterPage } from './components/RegisterPage'; -import { CompetitionPage } from './components/CompetitionPage'; -import { LandingPage } from './pages/LandingPage'; -import HeaderBar from './components/landing/HeaderBar'; -import AILearning from './components/AILearning'; -import { LanguageProvider, useLanguage } from './contexts/LanguageContext'; -import { AuthProvider, useAuth } from './contexts/AuthContext'; -import { t, type Language } from './i18n/translations'; -import { useSystemConfig } from './hooks/useSystemConfig'; +import { useEffect, useState } from 'react' +import useSWR from 'swr' +import { api } from './lib/api' +import { EquityChart } from './components/EquityChart' +import { AITradersPage } from './components/AITradersPage' +import { LoginPage } from './components/LoginPage' +import { RegisterPage } from './components/RegisterPage' +import { CompetitionPage } from './components/CompetitionPage' +import { LandingPage } from './pages/LandingPage' +import HeaderBar from './components/landing/HeaderBar' +import AILearning from './components/AILearning' +import { LanguageProvider, useLanguage } from './contexts/LanguageContext' +import { AuthProvider, useAuth } from './contexts/AuthContext' +import { t, type Language } from './i18n/translations' +import { useSystemConfig } from './hooks/useSystemConfig' +import { AlertTriangle } from 'lucide-react' import type { SystemStatus, AccountInfo, @@ -20,67 +21,76 @@ import type { DecisionRecord, Statistics, TraderInfo, -} from './types'; +} from './types' -type Page = 'competition' | 'traders' | 'trader'; +type Page = 'competition' | 'traders' | 'trader' // 获取友好的AI模型名称 function getModelDisplayName(modelId: string): string { switch (modelId.toLowerCase()) { case 'deepseek': - return 'DeepSeek'; + return 'DeepSeek' case 'qwen': - return 'Qwen'; + return 'Qwen' case 'claude': - return 'Claude'; + return 'Claude' default: - return modelId.toUpperCase(); + return modelId.toUpperCase() } } function App() { - const { language, setLanguage } = useLanguage(); - const { user, token, logout, isLoading } = useAuth(); - const { config: systemConfig, loading: configLoading } = useSystemConfig(); - const [route, setRoute] = useState(window.location.pathname); + const { language, setLanguage } = useLanguage() + const { user, token, logout, isLoading } = useAuth() + const { config: systemConfig, loading: configLoading } = useSystemConfig() + const [route, setRoute] = useState(window.location.pathname) // 从URL路径读取初始页面状态(支持刷新保持页面) const getInitialPage = (): Page => { - const path = window.location.pathname; - const hash = window.location.hash.slice(1); // 去掉 # - - if (path === '/traders' || hash === 'traders') return 'traders'; - if (path === '/dashboard' || hash === 'trader' || hash === 'details') return 'trader'; - return 'competition'; // 默认为竞赛页面 - }; + const path = window.location.pathname + const hash = window.location.hash.slice(1) // 去掉 # - const [currentPage, setCurrentPage] = useState(getInitialPage()); - const [selectedTraderId, setSelectedTraderId] = useState(); - const [lastUpdate, setLastUpdate] = useState('--:--:--'); + if (path === '/traders' || hash === 'traders') return 'traders' + if (path === '/dashboard' || hash === 'trader' || hash === 'details') + return 'trader' + return 'competition' // 默认为竞赛页面 + } + + const [currentPage, setCurrentPage] = useState(getInitialPage()) + const [selectedTraderId, setSelectedTraderId] = useState() + const [lastUpdate, setLastUpdate] = useState('--:--:--') // 监听URL变化,同步页面状态 useEffect(() => { const handleRouteChange = () => { - const path = window.location.pathname; - const hash = window.location.hash.slice(1); - - if (path === '/traders' || hash === 'traders') { - setCurrentPage('traders'); - } else if (path === '/dashboard' || hash === 'trader' || hash === 'details') { - setCurrentPage('trader'); - } else if (path === '/competition' || hash === 'competition' || hash === '') { - setCurrentPage('competition'); - } - setRoute(path); - }; + const path = window.location.pathname + const hash = window.location.hash.slice(1) - window.addEventListener('hashchange', handleRouteChange); - window.addEventListener('popstate', handleRouteChange); + if (path === '/traders' || hash === 'traders') { + setCurrentPage('traders') + } else if ( + path === '/dashboard' || + hash === 'trader' || + hash === 'details' + ) { + setCurrentPage('trader') + } else if ( + path === '/competition' || + hash === 'competition' || + hash === '' + ) { + setCurrentPage('competition') + } + setRoute(path) + } + + window.addEventListener('hashchange', handleRouteChange) + window.addEventListener('popstate', handleRouteChange) return () => { - window.removeEventListener('hashchange', handleRouteChange); - window.removeEventListener('popstate', handleRouteChange); - }; - }, []); + window.removeEventListener('hashchange', handleRouteChange) + window.removeEventListener('popstate', handleRouteChange) + } + }, []) // 切换页面时更新URL hash (当前通过按钮直接调用setCurrentPage,这个函数暂时保留用于未来扩展) // const navigateToPage = (page: Page) => { @@ -90,19 +100,19 @@ function App() { // 获取trader列表(仅在用户登录时) const { data: traders } = useSWR( - user && token ? 'traders' : null, - api.getTraders, + user && token ? 'traders' : null, + api.getTraders, { refreshInterval: 10000, } - ); + ) // 当获取到traders后,设置默认选中第一个 useEffect(() => { if (traders && traders.length > 0 && !selectedTraderId) { - setSelectedTraderId(traders[0].trader_id); + setSelectedTraderId(traders[0].trader_id) } - }, [traders, selectedTraderId]); + }, [traders, selectedTraderId]) // 如果在trader页面,获取该trader的数据 const { data: status } = useSWR( @@ -115,7 +125,7 @@ function App() { revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } - ); + ) const { data: account } = useSWR( currentPage === 'trader' && selectedTraderId @@ -127,7 +137,7 @@ function App() { revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } - ); + ) const { data: positions } = useSWR( currentPage === 'trader' && selectedTraderId @@ -139,7 +149,7 @@ function App() { revalidateOnFocus: false, // 禁用聚焦时重新验证,减少请求 dedupingInterval: 10000, // 10秒去重,防止短时间内重复请求 } - ); + ) const { data: decisions } = useSWR( currentPage === 'trader' && selectedTraderId @@ -151,7 +161,7 @@ function App() { revalidateOnFocus: false, dedupingInterval: 20000, } - ); + ) const { data: stats } = useSWR( currentPage === 'trader' && selectedTraderId @@ -163,62 +173,71 @@ function App() { revalidateOnFocus: false, dedupingInterval: 20000, } - ); + ) useEffect(() => { if (account) { - const now = new Date().toLocaleTimeString(); - setLastUpdate(now); + const now = new Date().toLocaleTimeString() + setLastUpdate(now) } - }, [account]); + }, [account]) - const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId); + const selectedTrader = traders?.find((t) => t.trader_id === selectedTraderId) // Handle routing useEffect(() => { const handlePopState = () => { - setRoute(window.location.pathname); - }; - window.addEventListener('popstate', handlePopState); - return () => window.removeEventListener('popstate', handlePopState); - }, []); + setRoute(window.location.pathname) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) // Set current page based on route for consistent navigation state useEffect(() => { if (route === '/competition') { - setCurrentPage('competition'); + setCurrentPage('competition') } else if (route === '/traders') { - setCurrentPage('traders'); + setCurrentPage('traders') } else if (route === '/dashboard') { - setCurrentPage('trader'); + setCurrentPage('trader') } - }, [route]); + }, [route]) // Show loading spinner while checking auth or config if (isLoading || configLoading) { return ( -
+
- NoFx Logo + NoFx Logo

{t('loading', language)}

- ); + ) } // Handle specific routes regardless of authentication if (route === '/login') { - return ; + return } if (route === '/register') { - return ; + return } if (route === '/competition') { return ( -
- + { - console.log('Competition page onPageChange called with:', page); - console.log('Current route:', route, 'Current page:', currentPage); - + console.log('Competition page onPageChange called with:', page) + console.log('Current route:', route, 'Current page:', currentPage) + if (page === 'competition') { - console.log('Navigating to competition'); - window.history.pushState({}, '', '/competition'); - setRoute('/competition'); - setCurrentPage('competition'); + console.log('Navigating to competition') + window.history.pushState({}, '', '/competition') + setRoute('/competition') + setCurrentPage('competition') } else if (page === 'traders') { - console.log('Navigating to traders'); - window.history.pushState({}, '', '/traders'); - setRoute('/traders'); - setCurrentPage('traders'); + console.log('Navigating to traders') + window.history.pushState({}, '', '/traders') + setRoute('/traders') + setCurrentPage('traders') } else if (page === 'trader') { - console.log('Navigating to trader/dashboard'); - window.history.pushState({}, '', '/dashboard'); - setRoute('/dashboard'); - setCurrentPage('trader'); + console.log('Navigating to trader/dashboard') + window.history.pushState({}, '', '/dashboard') + setRoute('/dashboard') + setCurrentPage('trader') } - - console.log('After navigation - route:', route, 'currentPage:', currentPage); + + console.log( + 'After navigation - route:', + route, + 'currentPage:', + currentPage + ) }} />
- ); + ) } - + // Show landing page for root route if (route === '/' || route === '') { - return ; + return } - + // Show main app for authenticated users on other routes if (!systemConfig?.admin_mode && (!user || !token)) { // Default to landing page when not authenticated and no specific route - return ; + return } return ( -
- + { - console.log('Main app onPageChange called with:', page); - + console.log('Main app onPageChange called with:', page) + if (page === 'competition') { - window.history.pushState({}, '', '/competition'); - setRoute('/competition'); - setCurrentPage('competition'); + window.history.pushState({}, '', '/competition') + setRoute('/competition') + setCurrentPage('competition') } else if (page === 'traders') { - window.history.pushState({}, '', '/traders'); - setRoute('/traders'); - setCurrentPage('traders'); + window.history.pushState({}, '', '/traders') + setRoute('/traders') + setCurrentPage('traders') } else if (page === 'trader') { - window.history.pushState({}, '', '/dashboard'); - setRoute('/dashboard'); - setCurrentPage('trader'); + window.history.pushState({}, '', '/dashboard') + setRoute('/dashboard') + setCurrentPage('trader') } }} /> @@ -301,12 +328,12 @@ function App() { {currentPage === 'competition' ? ( ) : currentPage === 'traders' ? ( - { - setSelectedTraderId(traderId); - window.history.pushState({}, '', '/dashboard'); - setRoute('/dashboard'); - setCurrentPage('trader'); + setSelectedTraderId(traderId) + window.history.pushState({}, '', '/dashboard') + setRoute('/dashboard') + setCurrentPage('trader') }} /> ) : ( @@ -327,8 +354,14 @@ function App() { {/* Footer */} -
-
+
+

{t('footerTitle', language)}

{t('footerWarning', language)}

@@ -337,20 +370,29 @@ function App() { target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 px-3 py-2 rounded text-sm font-semibold transition-all hover:scale-105" - style={{ background: '#1E2329', color: '#848E9C', border: '1px solid #2B3139' }} + style={{ + background: '#1E2329', + color: '#848E9C', + border: '1px solid #2B3139', + }} onMouseEnter={(e) => { - e.currentTarget.style.background = '#2B3139'; - e.currentTarget.style.color = '#EAECEF'; - e.currentTarget.style.borderColor = '#F0B90B'; + e.currentTarget.style.background = '#2B3139' + e.currentTarget.style.color = '#EAECEF' + e.currentTarget.style.borderColor = '#F0B90B' }} onMouseLeave={(e) => { - e.currentTarget.style.background = '#1E2329'; - e.currentTarget.style.color = '#848E9C'; - e.currentTarget.style.borderColor = '#2B3139'; + e.currentTarget.style.background = '#1E2329' + e.currentTarget.style.color = '#848E9C' + e.currentTarget.style.borderColor = '#2B3139' }} > - - + + GitHub @@ -358,7 +400,7 @@ function App() {
- ); + ) } // Trader Details Page Component @@ -374,17 +416,17 @@ function TraderDetailsPage({ selectedTraderId, onTraderSelect, }: { - selectedTrader?: TraderInfo; - traders?: TraderInfo[]; - selectedTraderId?: string; - onTraderSelect: (traderId: string) => void; - status?: SystemStatus; - account?: AccountInfo; - positions?: Position[]; - decisions?: DecisionRecord[]; - stats?: Statistics; - lastUpdate: string; - language: Language; + selectedTrader?: TraderInfo + traders?: TraderInfo[] + selectedTraderId?: string + onTraderSelect: (traderId: string) => void + status?: SystemStatus + account?: AccountInfo + positions?: Position[] + decisions?: DecisionRecord[] + stats?: Statistics + lastUpdate: string + language: Language }) { if (!selectedTrader) { return ( @@ -411,30 +453,52 @@ function TraderDetailsPage({
- ); + ) } return (
{/* Trader Header */} -
+
-

- +

+ 🤖 {selectedTrader.trader_name}

- + {/* Trader Selector */} {traders && traders.length > 0 && (
- {t('switchTrader', language)}: + + {t('switchTrader', language)}: + setCoinPool(e.target.value)} placeholder="https://api.example.com/coinpool" className="w-full px-3 py-2 rounded" - style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} + style={{ + background: '#0B0E11', + border: '1px solid #2B3139', + color: '#EAECEF', + }} />
{t('coinPoolDescription', language)} @@ -885,7 +1220,10 @@ function SignalSourceModal({
-
-
-
+
+
ℹ️ {t('information', language)}
@@ -932,7 +1283,7 @@ function SignalSourceModal({
- ); + ) } // Model Configuration Modal Component @@ -943,58 +1294,73 @@ function ModelConfigModal({ onSave, onDelete, onClose, - language + language, }: { - allModels: AIModel[]; - configuredModels: AIModel[]; - editingModelId: string | null; - onSave: (modelId: string, apiKey: string, baseUrl?: string, modelName?: string) => void; - onDelete: (modelId: string) => void; - onClose: () => void; - language: Language; + allModels: AIModel[] + configuredModels: AIModel[] + editingModelId: string | null + onSave: ( + modelId: string, + apiKey: string, + baseUrl?: string, + modelName?: string + ) => void + onDelete: (modelId: string) => void + onClose: () => void + language: Language }) { - const [selectedModelId, setSelectedModelId] = useState(editingModelId || ''); - const [apiKey, setApiKey] = useState(''); - const [baseUrl, setBaseUrl] = useState(''); - const [modelName, setModelName] = useState(''); + const [selectedModelId, setSelectedModelId] = useState(editingModelId || '') + const [apiKey, setApiKey] = useState('') + const [baseUrl, setBaseUrl] = useState('') + const [modelName, setModelName] = useState('') // 获取当前编辑的模型信息 - 编辑时从已配置的模型中查找,新建时从所有支持的模型中查找 const selectedModel = editingModelId - ? configuredModels?.find(m => m.id === selectedModelId) - : allModels?.find(m => m.id === selectedModelId); + ? configuredModels?.find((m) => m.id === selectedModelId) + : allModels?.find((m) => m.id === selectedModelId) // 如果是编辑现有模型,初始化API Key、Base URL和Model Name useEffect(() => { if (editingModelId && selectedModel) { - setApiKey(selectedModel.apiKey || ''); - setBaseUrl(selectedModel.customApiUrl || ''); - setModelName(selectedModel.customModelName || ''); + setApiKey(selectedModel.apiKey || '') + setBaseUrl(selectedModel.customApiUrl || '') + setModelName(selectedModel.customModelName || '') } - }, [editingModelId, selectedModel]); + }, [editingModelId, selectedModel]) const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (!selectedModelId || !apiKey.trim()) return; + e.preventDefault() + if (!selectedModelId || !apiKey.trim()) return - onSave(selectedModelId, apiKey.trim(), baseUrl.trim() || undefined, modelName.trim() || undefined); - }; + onSave( + selectedModelId, + apiKey.trim(), + baseUrl.trim() || undefined, + modelName.trim() || undefined + ) + } // 可选择的模型列表(所有支持的模型) - const availableModels = allModels || []; + const availableModels = allModels || [] return (
-
+

- {editingModelId ? t('editAIModel', language) : t('addAIModel', language)} + {editingModelId + ? t('editAIModel', language) + : t('addAIModel', language)}

{editingModelId && ( - )} +
+ {selectedExchange?.id === 'binance' && ( + + )} + {editingExchangeId && ( + + )} +
{!editingExchangeId && (
-