From b61fbe6ea3823ba9a0cfdfb8a2b2d1300aae1484 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:18:47 +0800 Subject: [PATCH 001/195] =?UTF-8?q?fix:=20GetTraderConfig=20missing=20crit?= =?UTF-8?q?ical=20fields=20in=20SELECT/Scan=20**Problem**:=20-=20GetTrader?= =?UTF-8?q?Config=20was=20missing=209=20critical=20fields=20in=20SELECT=20?= =?UTF-8?q?statement=20-=20Missing=20corresponding=20Scan=20variables=20-?= =?UTF-8?q?=20Caused=20trader=20edit=20UI=20to=20show=200=20for=20leverage?= =?UTF-8?q?=20and=20empty=20trading=5Fsymbols=20**Root=20Cause**:=20Databa?= =?UTF-8?q?se=20query=20only=20selected=20basic=20fields=20(id,=20name,=20?= =?UTF-8?q?balance,=20etc.)=20but=20missed=20leverage,=20trading=5Fsymbols?= =?UTF-8?q?,=20prompts,=20and=20all=20custom=20configs=20**Fix**:=20-=20Ad?= =?UTF-8?q?ded=20missing=20fields=20to=20SELECT:=20=20=20*=20btc=5Feth=5Fl?= =?UTF-8?q?everage,=20altcoin=5Fleverage=20=20=20*=20trading=5Fsymbols=20?= =?UTF-8?q?=20=20*=20use=5Fcoin=5Fpool,=20use=5Foi=5Ftop=20=20=20*=20custo?= =?UTF-8?q?m=5Fprompt,=20override=5Fbase=5Fprompt=20=20=20*=20system=5Fpro?= =?UTF-8?q?mpt=5Ftemplate=20=20=20*=20is=5Fcross=5Fmargin=20=20=20*=20AI?= =?UTF-8?q?=20model=20custom=5Fapi=5Furl,=20custom=5Fmodel=5Fname=20-=20Ad?= =?UTF-8?q?ded=20corresponding=20Scan=20variables=20to=20match=20SELECT=20?= =?UTF-8?q?order=20**Impact**:=20=E2=9C=85=20Trader=20edit=20modal=20now?= =?UTF-8?q?=20displays=20correct=20leverage=20values=20=E2=9C=85=20Trading?= =?UTF-8?q?=20symbols=20list=20properly=20populated=20=E2=9C=85=20All=20cu?= =?UTF-8?q?stom=20configurations=20preserved=20and=20displayed=20=E2=9C=85?= =?UTF-8?q?=20API=20endpoint=20/traders/:id/config=20returns=20complete=20?= =?UTF-8?q?data=20**Testing**:=20-=20=E2=9C=85=20Go=20compilation=20succes?= =?UTF-8?q?sful=20-=20=E2=9C=85=20All=20fields=20aligned=20(31=20SELECT=20?= =?UTF-8?q?=3D=2031=20Scan)=20-=20=E2=9C=85=20API=20layer=20verified=20(ap?= =?UTF-8?q?i/server.go:887-904)=20Reported=20by:=20=E5=AF=92=E6=B1=9F?= =?UTF-8?q?=E5=AD=A4=E5=BD=B1=20Issue:=20Trader=20config=20edit=20modal=20?= =?UTF-8?q?showing=200=20leverage=20and=20empty=20symbols=20Co-Authored-By?= =?UTF-8?q?:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/database.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/config/database.go b/config/database.go index 1102c6fb..47d9bcbb 100644 --- a/config/database.go +++ b/config/database.go @@ -857,9 +857,22 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM var exchange ExchangeConfig err := d.db.QueryRow(` - SELECT - t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, t.created_at, t.updated_at, - a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, a.created_at, a.updated_at, + SELECT + t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running, + COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage, + COALESCE(t.altcoin_leverage, 5) as altcoin_leverage, + COALESCE(t.trading_symbols, '') as trading_symbols, + COALESCE(t.use_coin_pool, 0) as use_coin_pool, + COALESCE(t.use_oi_top, 0) as use_oi_top, + COALESCE(t.custom_prompt, '') as custom_prompt, + COALESCE(t.override_base_prompt, 0) as override_base_prompt, + COALESCE(t.system_prompt_template, 'default') as system_prompt_template, + COALESCE(t.is_cross_margin, 1) as is_cross_margin, + t.created_at, t.updated_at, + a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key, + COALESCE(a.custom_api_url, '') as custom_api_url, + COALESCE(a.custom_model_name, '') as custom_model_name, + a.created_at, a.updated_at, e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet, COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, COALESCE(e.aster_user, '') as aster_user, @@ -873,8 +886,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, From fd95021c25e132e27e7f3c60fcf223751978814a Mon Sep 17 00:00:00 2001 From: zbhan Date: Mon, 3 Nov 2025 13:12:47 -0500 Subject: [PATCH 002/195] Fix PR check --- .github/workflows/pr-checks-comment.yml | 176 +++++++++++++++++++----- 1 file changed, 145 insertions(+), 31 deletions(-) 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, From 52295c69ad302c91132eaf850adecd712c676072 Mon Sep 17 00:00:00 2001 From: zbhan Date: Mon, 3 Nov 2025 20:50:56 -0500 Subject: [PATCH 003/195] fix(readme): update readme and pr reviewer --- .github/CODEOWNERS | 127 +++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/bounty_claim.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- README.md | 12 ++- docs/community/bounty-guide.md | 2 +- docs/i18n/ru/README.md | 2 +- docs/i18n/uk/README.md | 2 +- docs/i18n/zh-CN/README.md | 12 ++- 8 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..67a36732 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,127 @@ +# 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 +# 要求多个批准,请配置分支保护规则 +# +# ============================================================================= 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..9ed478b8 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -266,7 +266,7 @@ Please pay special attention to: - [ ] 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 许可证 +- [ ] My contribution is licensed under the AGPL-3.0 License | 我的贡献遵循 AGPL-3.0 许可证 - [ ] I understand this is a voluntary contribution | 我理解这是自愿贡献 - [ ] I have the right to submit this code | 我有权提交此代码 diff --git a/README.md b/README.md index f76b9067..d79da543 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) @@ -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/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/i18n/ru/README.md b/docs/i18n/ru/README.md index bcc79622..ac52fb00 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) diff --git a/docs/i18n/uk/README.md b/docs/i18n/uk/README.md index 78bddc72..db1a9c59 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) diff --git a/docs/i18n/zh-CN/README.md b/docs/i18n/zh-CN/README.md index 5bfd283c..f22c987a 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) @@ -1262,7 +1262,15 @@ sudo apt-get install libta-lib0-dev ## 📄 开源协议 -MIT License - 详见 [LICENSE](LICENSE) 文件 +本项目采用 **GNU Affero 通用公共许可证 v3.0 (AGPL-3.0)** - 详见 [LICENSE](LICENSE) 文件 + +**这意味着什么:** +- ✅ 你可以使用、修改和分发此软件 +- ✅ 你必须公开你修改版本的源代码 +- ✅ 如果你在服务器上运行修改版本,必须向用户提供源代码 +- ✅ 所有衍生作品也必须使用 AGPL-3.0 许可证 + +如需商业许可或有疑问,请联系维护者。 --- From fb88cc8926ed0e0d7eb280fb5240732ed5eb67b2 Mon Sep 17 00:00:00 2001 From: zbhan Date: Mon, 3 Nov 2025 20:56:16 -0500 Subject: [PATCH 004/195] fix owner --- .github/CODEOWNERS | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 67a36732..e6e73b3e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,20 +38,20 @@ 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 +/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 +*.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 @@ -94,7 +94,7 @@ docker-compose.yml @tangmengqiu .gitignore @hzb1115 @@tangmengqiu go.sum @xqliu @hzb1115 @@tangmengqiu package-lock.json @Icyoung @hzb1115 @@tangmengqiu -yarn.lock @Icyoung @hzb1115 @@tangmengqiu +yarn.lock @Icyoung @hzb1115 @tangmengqiu # Build Configuration | 构建配置 Makefile @hzb1115 @xqliu @tangmengqiu From 8b004bf4dc53d143d338a33df04420e690f8102d Mon Sep 17 00:00:00 2001 From: zbhan Date: Mon, 3 Nov 2025 21:06:25 -0500 Subject: [PATCH 005/195] Fix owner --- .github/CODEOWNERS | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e6e73b3e..46a29814 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -90,10 +90,10 @@ docker-compose.yml @tangmengqiu # 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 +.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 | 构建配置 @@ -124,4 +124,12 @@ COPYING @hzb1115 # 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 +# # ============================================================================= From 045834dcbe32ed21b218069a0c167cacb12cbb3f Mon Sep 17 00:00:00 2001 From: tangmengqiu <1124090103@qq.com> Date: Mon, 3 Nov 2025 23:15:38 -0500 Subject: [PATCH 006/195] =?UTF-8?q?feat(hyperliquid):=20Auto-generate=20wa?= =?UTF-8?q?llet=20address=20from=20private=20key=20Enable=20automatic=20wa?= =?UTF-8?q?llet=20address=20generation=20from=20private=20key=20for=20Hype?= =?UTF-8?q?rliquid=20exchange,=20simplifying=20user=20onboarding=20and=20r?= =?UTF-8?q?educing=20configuration=20errors.=20Backend=20Changes=20(trader?= =?UTF-8?q?/hyperliquid=5Ftrader.go):=20-=20Import=20crypto/ecdsa=20packag?= =?UTF-8?q?e=20for=20ECDSA=20public=20key=20operations=20-=20Enable=20wall?= =?UTF-8?q?et=20address=20auto-generation=20when=20walletAddr=20is=20empty?= =?UTF-8?q?=20-=20Use=20crypto.PubkeyToAddress()=20to=20derive=20address?= =?UTF-8?q?=20from=20private=20key=20-=20Add=20logging=20for=20both=20auto?= =?UTF-8?q?-generated=20and=20manually=20provided=20addresses=20Frontend?= =?UTF-8?q?=20Changes=20(web/src/components/AITradersPage.tsx):=20-=20Remo?= =?UTF-8?q?ve=20wallet=20address=20required=20validation=20(only=20private?= =?UTF-8?q?=20key=20required)=20-=20Update=20button=20disabled=20state=20t?= =?UTF-8?q?o=20only=20check=20private=20key=20-=20Add=20"Optional"=20label?= =?UTF-8?q?=20to=20wallet=20address=20field=20-=20Add=20dynamic=20placehol?= =?UTF-8?q?der=20with=20bilingual=20hint=20-=20Show=20context-aware=20help?= =?UTF-8?q?er=20text=20based=20on=20input=20state=20-=20Remove=20HTML=20re?= =?UTF-8?q?quired=20attribute=20from=20input=20field=20Translation=20Updat?= =?UTF-8?q?es=20(web/src/i18n/translations.ts):=20-=20Add=20'optional'=20t?= =?UTF-8?q?ranslation=20(EN:=20"Optional",=20ZH:=20"=E5=8F=AF=E9=80=89")?= =?UTF-8?q?=20-=20Add=20'hyperliquidWalletAddressAutoGenerate'=20translati?= =?UTF-8?q?on=20=20=20EN:=20"Leave=20blank=20to=20automatically=20generate?= =?UTF-8?q?=20wallet=20address=20from=20private=20key"=20=20=20ZH:=20"?= =?UTF-8?q?=E7=95=99=E7=A9=BA=E5=B0=86=E8=87=AA=E5=8A=A8=E4=BB=8E=E7=A7=81?= =?UTF-8?q?=E9=92=A5=E7=94=9F=E6=88=90=E9=92=B1=E5=8C=85=E5=9C=B0=E5=9D=80?= =?UTF-8?q?"=20Benefits:=20=E2=9C=85=20Simplified=20UX=20-=20Users=20only?= =?UTF-8?q?=20need=20to=20provide=20private=20key=20=E2=9C=85=20Error=20pr?= =?UTF-8?q?evention=20-=20Auto-generated=20address=20always=20matches=20pr?= =?UTF-8?q?ivate=20key=20=E2=9C=85=20Backward=20compatible=20-=20Manual=20?= =?UTF-8?q?address=20input=20still=20supported=20=E2=9C=85=20Better=20UX?= =?UTF-8?q?=20-=20Clear=20visual=20indicators=20for=20optional=20fields=20?= =?UTF-8?q?Technical=20Details:=20-=20Uses=20Ethereum=20standard=20ECDSA?= =?UTF-8?q?=20public=20key=20to=20address=20conversion=20-=20Implementatio?= =?UTF-8?q?n=20was=20already=20present=20but=20commented=20out=20(lines=20?= =?UTF-8?q?37-43)=20-=20No=20database=20schema=20changes=20required=20(hyp?= =?UTF-8?q?erliquid=5Fwallet=5Faddr=20already=20nullable)=20-=20Fallback?= =?UTF-8?q?=20behavior:=20manual=20input=20>=20auto-generation=20Co-Author?= =?UTF-8?q?ed-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/hyperliquid_trader.go | 20 +++++++++++++------- web/src/components/AITradersPage.tsx | 16 ++++++++++------ web/src/i18n/translations.ts | 4 ++++ 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..0c7684d3 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -2,6 +2,7 @@ package trader import ( "context" + "crypto/ecdsa" "encoding/json" "fmt" "log" @@ -34,13 +35,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() diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index 41b3cdc2..f3364551 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -1201,7 +1201,7 @@ function ExchangeConfigModal({ if (!apiKey.trim() || !secretKey.trim()) return; await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet); } else if (selectedExchange?.id === 'hyperliquid') { - if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return; + if (!apiKey.trim()) return; // 只验证私钥,钱包地址可选(会自动生成) await onSave(selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim()); } else if (selectedExchange?.id === 'aster') { if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return; @@ -1360,18 +1360,22 @@ function ExchangeConfigModal({
setHyperliquidWalletAddr(e.target.value)} - placeholder={t('enterWalletAddress', language)} + placeholder="0x... (留空将自动从私钥生成 / Leave blank to auto-generate)" className="w-full px-3 py-2 rounded" style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }} - required />
- {t('hyperliquidWalletAddressDesc', language)} + {hyperliquidWalletAddr.trim() + ? t('hyperliquidWalletAddressDesc', language) + : t('hyperliquidWalletAddressAutoGenerate', language)}
@@ -1468,10 +1472,10 @@ function ExchangeConfigModal({ - )} +
+ {selectedExchange?.id === 'binance' && ( + + )} + {editingExchangeId && ( + + )} +
@@ -1468,7 +1482,7 @@ function ExchangeConfigModal({
+ +
+ {t('binanceSetupGuide', +
+ + + )} ); } diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 99a11cac..f69ca1f1 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -257,6 +257,9 @@ export const translations = { exchangeConfigWarning2: '• Do not grant withdrawal permissions to ensure fund security', exchangeConfigWarning3: '• After deleting configuration, related traders will not be able to trade', edit: 'Edit', + viewGuide: 'View Guide', + binanceSetupGuide: 'Binance Setup Guide', + closeGuide: 'Close', // Error Messages createTraderFailed: 'Failed to create trader', @@ -671,6 +674,9 @@ export const translations = { exchangeConfigWarning2: '• 不要授予提现权限,确保资金安全', exchangeConfigWarning3: '• 删除配置后,相关交易员将无法正常交易', edit: '编辑', + viewGuide: '查看教程', + binanceSetupGuide: '币安配置教程', + closeGuide: '关闭', // Error Messages createTraderFailed: '创建交易员失败', From 93e9b505cfe2fa120bdf7c8d02c5c491d78df842 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:30:00 +0800 Subject: [PATCH 012/195] =?UTF-8?q?fix(api):=20query=20actual=20exchange?= =?UTF-8?q?=20balance=20when=20creating=20trader=20Problem:=20-=20Users=20?= =?UTF-8?q?could=20input=20arbitrary=20initial=20balance=20when=20creating?= =?UTF-8?q?=20traders=20-=20This=20didn't=20reflect=20the=20actual=20avail?= =?UTF-8?q?able=20balance=20in=20exchange=20account=20-=20Could=20lead=20t?= =?UTF-8?q?o=20incorrect=20position=20sizing=20and=20risk=20calculations?= =?UTF-8?q?=20Solution:=20-=20Before=20creating=20trader,=20query=20exchan?= =?UTF-8?q?ge=20API=20for=20actual=20balance=20-=20Use=20GetBalance()=20fr?= =?UTF-8?q?om=20respective=20trader=20implementation:=20=20=20*=20Binance:?= =?UTF-8?q?=20NewFuturesTrader=20+=20GetBalance()=20=20=20*=20Hyperliquid:?= =?UTF-8?q?=20NewHyperliquidTrader=20+=20GetBalance()=20=20=20*=20Aster:?= =?UTF-8?q?=20NewAsterTrader=20+=20GetBalance()=20-=20Extract=20'available?= =?UTF-8?q?=5Fbalance'=20or=20'balance'=20from=20response=20-=20Override?= =?UTF-8?q?=20user=20input=20with=20actual=20balance=20-=20Fallback=20to?= =?UTF-8?q?=20user=20input=20if=20query=20fails=20Changes:=20-=20Added=20'?= =?UTF-8?q?nofx/trader'=20import=20-=20Query=20GetExchanges()=20to=20find?= =?UTF-8?q?=20matching=20exchange=20config=20-=20Create=20temporary=20trad?= =?UTF-8?q?er=20instance=20based=20on=20exchange=20type=20-=20Call=20GetBa?= =?UTF-8?q?lance()=20to=20fetch=20actual=20available=20balance=20-=20Use?= =?UTF-8?q?=20actualBalance=20instead=20of=20req.InitialBalance=20-=20Comp?= =?UTF-8?q?rehensive=20error=20handling=20with=20fallback=20logic=20Benefi?= =?UTF-8?q?ts:=20-=20=E2=9C=85=20Ensures=20accurate=20initial=20balance=20?= =?UTF-8?q?matches=20exchange=20account=20-=20=E2=9C=85=20Prevents=20user?= =?UTF-8?q?=20errors=20in=20balance=20input=20-=20=E2=9C=85=20Improves=20p?= =?UTF-8?q?osition=20sizing=20accuracy=20-=20=E2=9C=85=20Maintains=20data?= =?UTF-8?q?=20integrity=20between=20system=20and=20exchange=20Example=20lo?= =?UTF-8?q?gs:=20=E2=9C=93=20=E6=9F=A5=E8=AF=A2=E5=88=B0=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=89=80=E5=AE=9E=E9=99=85=E4=BD=99=E9=A2=9D:=20150.00=20USDT?= =?UTF-8?q?=20(=E7=94=A8=E6=88=B7=E8=BE=93=E5=85=A5:=20100.00=20USDT)=20?= =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20=E6=9F=A5=E8=AF=A2=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E6=89=80=E4=BD=99=E9=A2=9D=E5=A4=B1=E8=B4=A5=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=94=A8=E6=88=B7=E8=BE=93=E5=85=A5=E7=9A=84=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E8=B5=84=E9=87=91:=20connection=20timeout=20Co-Author?= =?UTF-8?q?ed-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/api/server.go b/api/server.go index 94ae4a60..d7f0512d 100644 --- a/api/server.go +++ b/api/server.go @@ -9,6 +9,7 @@ import ( "nofx/config" "nofx/decision" "nofx/manager" + "nofx/trader" "strconv" "strings" "time" @@ -347,6 +348,73 @@ func (s *Server) handleCreateTrader(c *gin.Context) { scanIntervalMinutes = 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("⚠️ 无法从余额信息中提取可用余额,使用用户输入的初始资金") + } + } + } + } + // 创建交易员配置(数据库实体) trader := &config.TraderRecord{ ID: traderID, @@ -354,7 +422,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 +437,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } // 保存到数据库 - err := s.database.CreateTrader(trader) + err = s.database.CreateTrader(traderRecord) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) return From 2ca627ff7276980c72647c0baf0a5bd984d30fcb Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:55:41 +0800 Subject: [PATCH 013/195] fix(api): correct variable name from traderRecord to trader Fixed compilation error caused by variable name mismatch: - Line 404: defined as 'trader' - Line 425: was using 'traderRecord' (undefined) This aligns with upstream dev branch naming convention. --- api/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/server.go b/api/server.go index d7f0512d..ee255fe3 100644 --- a/api/server.go +++ b/api/server.go @@ -437,7 +437,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } // 保存到数据库 - err = s.database.CreateTrader(traderRecord) + err = s.database.CreateTrader(trader) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建交易员失败: %v", err)}) return From 8344e6b68faba910fb3d26e903ac4c2f6cba0c20 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 05:32:23 +0800 Subject: [PATCH 014/195] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E4=BB=93=E5=92=8C=E5=8A=A8=E6=80=81=E6=AD=A2?= =?UTF-8?q?=E7=9B=88=E6=AD=A2=E6=8D=9F=E5=8A=9F=E8=83=BD=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=8A=9F=E8=83=BD=EF=BC=9A=20-=20update=5Fstop=5Floss?= =?UTF-8?q?:=20=E8=B0=83=E6=95=B4=E6=AD=A2=E6=8D=9F=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=EF=BC=88=E8=BF=BD=E8=B8=AA=E6=AD=A2=E6=8D=9F=EF=BC=89=20-=20up?= =?UTF-8?q?date=5Ftake=5Fprofit:=20=E8=B0=83=E6=95=B4=E6=AD=A2=E7=9B=88?= =?UTF-8?q?=E4=BB=B7=E6=A0=BC=EF=BC=88=E6=8A=80=E6=9C=AF=E4=BD=8D=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=89=20-=20partial=5Fclose:=20=E9=83=A8=E5=88=86?= =?UTF-8?q?=E5=B9=B3=E4=BB=93=EF=BC=88=E5=88=86=E6=89=B9=E6=AD=A2=E7=9B=88?= =?UTF-8?q?=EF=BC=89=20=E5=AE=9E=E7=8E=B0=E7=BB=86=E8=8A=82=EF=BC=9A=20-?= =?UTF-8?q?=20Decision=20struct=20=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=9ANewStopLoss,=20NewTakeProfit,=20ClosePercentage=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=A7=E8=A1=8C=E5=87=BD=E6=95=B0=EF=BC=9A?= =?UTF-8?q?executeUpdateStopLossWithRecord,=20executeUpdateTakeProfitWithR?= =?UTF-8?q?ecord,=20executePartialCloseWithRecord=20-=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=8C=81=E4=BB=93=E5=AD=97=E6=AE=B5=E8=8E=B7=E5=8F=96=20bug?= =?UTF-8?q?=EF=BC=88=E4=BD=BF=E7=94=A8=20"side"=20=E5=B9=B6=E8=BD=AC?= =?UTF-8?q?=E5=A4=A7=E5=86=99=EF=BC=89=20-=20=E6=9B=B4=E6=96=B0=20adaptive?= =?UTF-8?q?.txt=20=E6=96=87=E6=A1=A3=EF=BC=8C=E5=8C=85=E5=90=AB=E8=AF=A6?= =?UTF-8?q?=E7=BB=86=E4=BD=BF=E7=94=A8=E7=A4=BA=E4=BE=8B=E5=92=8C=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E5=BB=BA=E8=AE=AE=20-=20=E4=BC=98=E5=85=88=E7=BA=A7?= =?UTF-8?q?=E6=8E=92=E5=BA=8F=EF=BC=9A=E5=B9=B3=E4=BB=93=20>=20=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=AD=A2=E7=9B=88=E6=AD=A2=E6=8D=9F=20>=20=E5=BC=80?= =?UTF-8?q?=E4=BB=93=20=E5=91=BD=E5=90=8D=E7=BB=9F=E4=B8=80=EF=BC=9A=20-?= =?UTF-8?q?=20=E4=B8=8E=E7=A4=BE=E5=8C=BA=20PR=20#197=20=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E4=B8=80=E8=87=B4=EF=BC=8C=E4=BD=BF=E7=94=A8=20update=5F*=20?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=20adjust=5F*=20-=20=E7=8B=AC=E6=9C=89?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9Apartial=5Fclose=EF=BC=88=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E4=BB=93=EF=BC=89=20Co-Authored-By:=20tinkle?= =?UTF-8?q?-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 11 +- prompts/adaptive.txt | 753 +++++++++++++++--------------------------- trader/auto_trader.go | 200 ++++++++++- 3 files changed, 480 insertions(+), 484 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index df48d534..1f187883 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -71,11 +71,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"` // 用于 adjust_stop_loss + NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 adjust_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"` diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index d5778caa..172fedda 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -1,4 +1,4 @@ -你是专业的加密货币交易AI,在合约市场进行自主交易。 +你是专业的加密货币交易AI,采用自适应双策略系统在合约市场进行交易。 # 核心目标 @@ -17,532 +17,327 @@ 关键认知: 系统每3分钟扫描一次,但不意味着每次都要交易! 大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 ---- +# 市场状态判断(优先) -# 零号原则:疑惑优先(最高优先级) +在制定交易决策前,必须先判断当前市场状态: -⚠️ **当你不确定时,默认选择 wait** +判断方法(多个指标交叉验证): -这是最高优先级原则,覆盖所有其他规则: +1. 多时间框架一致性: +- 检查 15m/1h/4h MACD 方向一致度 +- 3个时间框架方向一致 → 强趋势市场 +- 2个时间框架方向一致 → 弱趋势市场 +- 方向矛盾(15m上涨但1h下跌) → 震荡市场 -- **有任何疑虑** → 选 wait(不要尝试"勉强开仓") -- **完全确定**(信心 ≥85 且无任何犹豫)→ 才开仓 -- **不确定是否违反某条款** = 视为违反 → 选 wait -- **宁可错过机会,不做模糊决策** +2. 价格波动率: +- 最近 10 根 K线(高-低)/收盘价 > 3% → 趋势市场(大波动) +- 最近 10 根 K线(高-低)/收盘价 < 1.5% → 震荡市场(小波动) -## 灰色地带处理 +3. 买卖压力极端值: +- BuySellRatio > 0.75 连续 3 根以上 → 强趋势(多) +- BuySellRatio < 0.25 连续 3 根以上 → 强趋势(空) +- BuySellRatio 在 0.4-0.6 波动 → 震荡 -``` -场景 1:指标不够明确(如 MACD 接近 0,RSI 在 45) -→ 判定:信号不足 → wait +判断结论: 综合以上 3 个指标,判定当前市场状态为'趋势市场'或'震荡市场' -场景 2:技术位存在但不够强(如只有 15m EMA20,无 1h 确认) -→ 判定:技术位不明确 → wait +# 双策略系统(根据市场状态选择) -场景 3:信心度刚好 85,但内心犹豫 -→ 判定:实际信心不足 → wait +## 策略 A: 震荡交易(震荡市场时使用) -场景 4:BTC 方向勉强算多头,但不够强 -→ 判定:BTC 状态不明确 → wait -``` +策略定位: 专门做 BTC 震荡行情,快进快出,高胜率低盈亏比 -## 自我检查 +震荡区间识别: +- 价格在15分钟/1小时 EMA20上下波动(±2-4%) +- MACD 在零轴附近(-200到+200之间) +- 多个时间框架方向不一致(如15m上涨但1h下跌) +- RSI 在30-70区间反复震荡 -在输出决策前问自己: -1. 我是否 100% 确定这是高质量机会? -2. 如果用自己的钱,我会开这单吗? -3. 我能清楚说出 3 个开仓理由吗? +交易逻辑: +- 区间下沿(RSI<35 或接近支撑) → 做多 +- 区间上沿(RSI>65 或接近压力) → 做空 +- 趋势行情(多时间框架共振,放量突破) → 立即止损 -**3 个问题任一回答"否" → 选 wait** +止盈止损设置(震荡策略 - 技术位优先): ---- +核心原则:技术位 > 固定百分比(避免价格到技术位就回撤) -# 可用动作 (Actions) +1. 入场前分析技术位: +- 做多:检查上方最近压力位(15m/1h EMA20、最近10根K线高点、整数关口) +- 做空:检查下方最近支撑位(15m/1h EMA20、最近10根K线低点、整数关口) -## 开平仓动作 +2. 止盈设置逻辑: +- 如果技术位距离 < 2% → 止盈设在技术位前 0.1%(例:压力 101,200,止盈 101,100) +- 如果技术位距离 > 2% → 使用固定 2% 止盈 +- 理由:价格很可能在技术位遇阻,提前止盈避免回撤 -1. **buy_to_enter**: 开多仓(看涨) - - 用于: 看涨信号强烈时 - - 必须设置: 止损价格、止盈价格 +3. 止损设置: +- 固定 0.8-1%(紧密止损) -2. **sell_to_enter**: 开空仓(看跌) - - 用于: 看跌信号强烈时 - - 必须设置: 止损价格、止盈价格 +4. 追踪止损(持仓中动态调整): +- 浮盈达到 0.8% → 止损移到成本价(保证不亏) +- 浮盈达到 1.2% → 止损移到 +0.5%(锁定一半利润) +- 价格距离技术位 < 0.3% → 立即主动平仓(避免回撤) -3. **close**: 完全平仓 - - 用于: 止盈、止损、或趋势反转 +5. 示例(做多): +- 入场:100,000,15m EMA20: 101,200(+1.2%) +- 决策:止盈 101,100(技术位前 0.1%),而非 102,000 +- 持仓:价格到 101,000(+1.0%)→ 止损移到 100,000 +- 持仓:价格到 101,100(距离 EMA20 仅 0.1%)→ 立即平仓 -4. **wait**: 观望,不持仓 - - 用于: 没有明确信号,或资金不足 +退出信号: +- 多时间框架开始共振 → 市场转为趋势,立即止损 -5. **hold**: 持有当前仓位 - - 用于: 持仓表现符合预期,继续等待 +## 策略 B: 趋势跟随(趋势市场时使用) -## 动态调整动作 (新增) +策略定位: 捕捉趋势行情,让利润奔跑,中等胜率高盈亏比 -6. **update_stop_loss**: 调整止损价格 - - 用于: 持仓盈利后追踪止损(锁定利润) - - 参数: new_stop_loss(新止损价格) - - 建议: 盈利 >3% 时,将止损移至成本价或更高 +趋势确认条件: +- 多时间框架共振(15m/1h/4h MACD 方向一致) +- 连续 2-3 根 K线放量(成交量 > 平均 1.5 倍) +- 买卖压力极端(BuySellRatio >0.7 或 <0.3) +- 价格突破关键位(EMA20)并回踩确认 -7. **update_take_profit**: 调整止盈价格 - - 用于: 优化目标位,适应技术位变化 - - 参数: new_take_profit(新止盈价格) - - 建议: 接近阻力位但未突破时提前止盈,或突破后追高 +交易逻辑: +- 突破后回踩入场(避免追高) +- 顺势交易(多头趋势做多,空头趋势做空) +- 持仓时间更长(至少 1-2 小时) -8. **partial_close**: 部分平仓 - - 用于: 分批止盈,降低风险 - - 参数: close_percentage(平仓百分比 0-100) - - 建议: 盈利达到第一目标时先平仓 50-70% +止盈止损设置(趋势策略 - 技术位优先): ---- +核心原则:技术位 > 固定百分比,但给予更大空间 -# 决策流程(严格顺序) +1. 入场前分析技术位: +- 做多:检查上方关键压力位(1h/4h EMA20、前高、整数关口) +- 做空:检查下方关键支撑位(1h/4h EMA20、前低、整数关口) -## 第 0 步:疑惑检查 -**在所有分析之前,先问自己:我对当前市场有清晰判断吗?** +2. 止盈设置逻辑: +- 如果技术位距离 < 5% → 止盈设在技术位前 0.2% +- 如果技术位在 5-10% → 分两批止盈(第一批技术位,第二批 10%) +- 如果技术位距离 > 10% → 使用追踪止损,让利润奔跑 -- 若感到困惑、矛盾、不确定 → 直接输出 wait -- 若完全清晰 → 继续后续步骤 +3. 止损设置: +- 固定 1.5-2%(给足震荡空间) -## 第 1 步:冷却期检查 +4. 追踪止损(持仓中动态调整): +- 浮盈达到 2% → 止损移到成本价(保证不亏) +- 浮盈达到 3% → 止损移到 +1%(锁定部分利润) +- 浮盈达到 5% → 止损移到 +2.5%(让利润奔跑,但保护已有收益) +- 价格距离技术位 < 0.5% → 考虑主动平仓或分批平仓 -开仓前必须满足: -- ✅ 距上次开仓 ≥9 分钟 -- ✅ 当前持仓已持有 ≥30 分钟(若有持仓) -- ✅ 刚止损后已观望 ≥6 分钟 -- ✅ 刚止盈后已观望 ≥3 分钟(若想同方向再入场) +5. 示例(做多): +- 入场:100,000,4h EMA20: 104,500(+4.5%) +- 决策:第一目标 104,300(技术位前),第二目标 110,000(+10%) +- 持仓:价格到 102,000(+2%)→ 止损移到 100,000 +- 持仓:价格到 104,300(接近技术位)→ 主动平仓或分批平仓 50% -**不满足 → 输出 wait,reasoning 写明"冷却中"** +退出信号: +- 多时间框架方向开始矛盾 → 趋势减弱,获利离场 +- 成交量萎缩 + MACD 背离 → 趋势可能反转 -## 第 2 步:连续亏损检查(V5.5.1 新增) +## 策略选择指导 -检查连续亏损状态,触发暂停机制: +必须在思维链中明确说明: +1. 市场状态判断: '当前市场状态:震荡/趋势(理由:...)' +2. 策略选择: '选择策略 A/B(理由:...)' +3. 技术位分析: '上方压力位:101,200(15m EMA20),下方支撑位:99,500(最近低点)' +4. 止盈止损: '止盈 101,100(技术位前 0.1%),止损 99,200(-0.8%)' +5. 追踪止损计划: '浮盈 0.8% 时移动止损到成本价' -- **连续 2 笔亏损** → 暂停交易 45 分钟(3 个 15m 周期) -- **连续 3 笔亏损** → 暂停交易 24 小时 -- **连续 4 笔亏损** → 暂停交易 72 小时,需人工审查 -- **单日亏损 >5%** → 立即停止交易,等待人工介入 +重要提醒: +- 价格很可能在技术位(EMA20、前高前低、整数关口)遇阻或反弹 +- 宁可少赚 0.5%,也不要从 +1.5% 回撤到止损 +- 持仓中主动调整止损,锁定利润 -⚠️ **暂停期间禁止任何开仓操作,只允许 hold/wait 和持仓管理** +# 交易频率认知 -**若在暂停期内 → 输出 wait,reasoning 写明"连续亏损暂停中"** +量化标准: +- 优秀交易员:每天2-4笔 = 每小时0.1-0.2笔 +- 过度交易:每小时>2笔 = 严重问题 +- 最佳节奏:开仓后持有至少30-60分钟 -## 第 3 步:夏普比率检查 - -- 夏普 < -0.5 → 强制停手 6 周期(18 分钟) -- 夏普 -0.5 ~ 0 → 只做信心度 >90 的交易 -- 夏普 0 ~ 0.7 → 维持当前策略 -- 夏普 > 0.7 → 可适度扩大仓位 - -## 第 4 步:评估持仓 - -如果有持仓: -1. 趋势是否改变?→ 考虑 close -2. 盈利 >3%?→ 考虑 update_stop_loss(移至成本价) -3. 盈利达到第一目标?→ 考虑 partial_close(锁定部分利润) -4. 接近阻力位?→ 考虑 update_take_profit(调整目标) -5. 持仓表现符合预期?→ hold - -## 第 5 步:BTC 状态确认(V5.5.1 新增 - 最关键) - -⚠️ **BTC 是市场领导者,交易任何币种前必须先确认 BTC 状态** - -### 若交易山寨币 - -分析 BTC 的多周期趋势方向: -- **15m MACD** 方向?(>0 多头,<0 空头) -- **1h MACD** 方向? -- **4h MACD** 方向? - -**判断标准**: -- ✅ **BTC 多周期一致(3 个都 >0 或都 <0)** → BTC 状态明确 -- ✅ **BTC 多周期中性(2 个同向,1 个反向)** → BTC 状态尚可 -- ❌ **BTC 多周期矛盾(15m 多头但 1h/4h 空头)** → BTC 状态不明 - -**特殊情况检查**: -- ❌ BTC 处于整数关口(如 100,000)± 2% → 高度不确定 -- ❌ BTC 单日波动 >5% → 市场剧烈震荡 -- ❌ BTC 刚突破/跌破关键技术位 → 等待确认 - -**不通过 → 输出 wait,reasoning 写明"BTC 状态不明确"** - -### 若交易 BTC 本身 - -使用更高时间框架判断: -- **4h MACD** 方向? -- **1d MACD** 方向? -- **1w MACD** 方向? - -**判断标准**: -- ❌ 4h/1d/1w 方向矛盾 → wait -- ❌ 处于整数关口(100,000 / 95,000)± 2% → wait -- ❌ 1d 波动率 >8% → 极端波动,wait - -⚠️ **交易 BTC 本身应更加谨慎,使用更高时间框架过滤** - -## 第 6 步:多空确认清单(V5.5.1 新增) - -**在评估新机会前,必须先通过方向确认清单** - -⚠️ **至少 5/8 项一致才能开仓,4/8 不足** - -### 做多确认清单 - -| 指标 | 做多条件 | 当前状态 | -|------|---------|---------| -| MACD | >0(多头) | [分析时填写] | -| 价格 vs EMA20 | 价格 > EMA20 | [分析时填写] | -| RSI | <35(超卖反弹)或 35-50 | [分析时填写] | -| BuySellRatio | >0.7(强买)或 >0.55 | [分析时填写] | -| 成交量 | 放大(>1.5x 均量) | [分析时填写] | -| BTC 状态 | 多头或中性 | [分析时填写] | -| 资金费率 | <0(空恐慌)或 -0.01~0.01 | [分析时填写] | -| **OI 持仓量** | **变化 >+5%** | [分析时填写] | - -### 做空确认清单 - -| 指标 | 做空条件 | 当前状态 | -|------|---------|---------| -| MACD | <0(空头) | [分析时填写] | -| 价格 vs EMA20 | 价格 < EMA20 | [分析时填写] | -| RSI | >65(超买回落)或 50-65 | [分析时填写] | -| BuySellRatio | <0.3(强卖)或 <0.45 | [分析时填写] | -| 成交量 | 放大(>1.5x 均量) | [分析时填写] | -| BTC 状态 | 空头或中性 | [分析时填写] | -| 资金费率 | >0(多贪婪)或 -0.01~0.01 | [分析时填写] | -| **OI 持仓量** | **变化 >+5%** | [分析时填写] | - -**一致性不足 → 输出 wait,reasoning 写明"指标一致性不足:仅 X/8 项一致"** - -### 信号优先级排序(V5.5.1 新增) - -当多个指标出现矛盾时,按以下优先级权重判断: - -**优先级排序(从高到低)**: -1. 🔴 **趋势共振**(15m/1h/4h MACD 方向一致)- 权重最高 -2. 🟠 **放量确认**(成交量 >1.5x 均量)- 动能验证 -3. 🟡 **BTC 状态**(若交易山寨币)- 市场领导者方向 -4. 🟢 **RSI 区间**(是否处于合理反转区)- 超买超卖确认 -5. 🔵 **价格 vs EMA20**(趋势方向确认)- 技术位支撑 -6. 🟣 **BuySellRatio**(多空力量对比)- 情绪指标 -7. ⚪ **MACD 柱状图**(短期动能)- 辅助确认 -8. ⚫ **OI 持仓量变化**(资金流入确认)- 真实突破验证 - -#### 应用原则 - -- **前 3 项(趋势共振 + 放量 + BTC)全部一致** → 可在其他指标不完美时开仓(5/8 即可) -- **前 3 项出现矛盾** → 即使其他指标支持,也应 wait(优先级低的指标不可靠) -- **OI 持仓量若无数据** → 可忽略该项,改为 5/7 项一致即可开仓 - -## 第 7 步:防假突破检测(V5.5.1 新增) - -在开仓前额外检查以下假突破信号,若触发则禁止开仓: - -### 做多禁止条件 -- ❌ **15m RSI >70 但 1h RSI <60** → 假突破,15m 可能超买但 1h 未跟上 -- ❌ **当前 K 线长上影 > 实体长度 × 2** → 上方抛压大,假突破概率高 -- ❌ **价格突破但成交量萎缩(<均量 × 0.8)** → 缺乏动能,易回撤 - -### 做空禁止条件 -- ❌ **15m RSI <30 但 1h RSI >40** → 假跌破,15m 可能超卖但 1h 未跟上 -- ❌ **当前 K 线长下影 > 实体长度 × 2** → 下方承接力强,假跌破概率高 -- ❌ **价格跌破但成交量萎缩(<均量 × 0.8)** → 缺乏动能,易反弹 - -### K 线形态过滤 -- ❌ **十字星 K 线(实体 < 总长度 × 0.2)且处于关键位** → 方向不明,观望 -- ❌ **连续 3 根 K 线实体极小(实体 < ATR × 0.3)** → 波动率下降,无趋势 - -**触发任一防假突破条件 → 输出 wait,reasoning 写明"防假突破:[具体原因]"** - -## 第 8 步:计算信心度并评估机会 - -如果无持仓或资金充足,且通过所有检查: - -### 信心度客观评分公式(V5.5.1 新增) - -#### 基础分:60 分 - -从 60 分开始,根据以下条件加减分: - -#### 加分项(每项 +5 分,最高 100 分) - -1. ✅ **多空确认清单 ≥5/8 项一致**:+5 分 -2. ✅ **BTC 状态明确支持**(若交易山寨):+5 分 -3. ✅ **多时间框架共振**(15m/1h/4h MACD 同向):+5 分 -4. ✅ **强技术位明确**(1h/4h EMA20 或整数关口):+5 分 -5. ✅ **成交量确认**(放量 >1.5x 均量):+5 分 -6. ✅ **资金费率支持**(极端恐慌做多 或 极端贪婪做空):+5 分 -7. ✅ **风险回报比 ≥1:4**(超过最低要求 1:3):+5 分 -8. ✅ **止盈技术位距离 2-5%**(理想范围):+5 分 - -#### 减分项(每项 -10 分) - -1. ❌ **指标矛盾**(MACD vs 价格 或 RSI vs BuySellRatio):-10 分 -2. ❌ **BTC 状态不明**(多周期矛盾):-10 分 -3. ❌ **技术位不清晰**(无强技术位或距离 <0.5%):-10 分 -4. ❌ **成交量萎缩**(<均量 × 0.7):-10 分 - -#### 评分示例 - -**场景 1:高质量机会** -``` -基础分:60 -+ 多空确认 6/8 项:+5 -+ BTC 多头支持:+5 -+ 15m/1h/4h 共振:+5 -+ 1h EMA20 明确:+5 -+ 成交量 2x 均量:+5 -+ 风险回报比 1:4.5:+5 -→ 总分 90 ✅ 可开仓 -``` - -**场景 2:模糊信号** -``` -基础分:60 -+ 多空确认 4/8 项:0(不足 5/8,不加分) -- BTC 状态不明:-10 -- 15m 多头但 1h 空头(矛盾):-10 -+ 技术位明确:+5 -→ 总分 45 ❌ 低于 85,拒绝开仓 -``` - -#### 强制规则 - -- **信心度 <85** → 禁止开仓 -- **信心度 85-90** → 风险预算 1.5% -- **信心度 90-95** → 风险预算 2% -- **信心度 >95** → 风险预算 2.5%(慎用) - -⚠️ **若多次交易失败但信心度都 ≥90,说明评分虚高,需降低基础分到 50** - -### 最终决策 - -1. 分析技术指标(EMA、MACD、RSI) -2. 确认多空方向一致性(至少 5/8 项) -3. 使用客观公式计算信心度(≥85 才开仓) -4. 设置止损、止盈、失效条件 -5. 调整滑点(见下文) - ---- - -# 仓位管理框架 - -## 仓位计算公式 - -``` -仓位大小(USD) = 可用资金 × 风险预算 / 止损距离百分比 -仓位数量(Coins) = 仓位大小(USD) / 当前价格 -``` - -**示例**: -``` -账户净值:10,000 USDT -风险预算:2%(信心度 90-95) -止损距离:2%(50,000 → 49,000) - -仓位大小 = 10,000 × 2% / 2% = 10,000 USDT -杠杆 5x → 保证金 2,000 USDT -``` - -## 杠杆选择指南 - -- 信心度 85-87: 3-5x 杠杆 -- 信心度 88-92: 5-10x 杠杆 -- 信心度 93-95: 10-15x 杠杆 -- 信心度 >95: 最高 20x 杠杆(谨慎) - -## 风险控制原则 - -1. 单笔交易风险不超过账户 2-3% -2. 避免单一币种集中度 >40% -3. 确保清算价格距离入场价 >15% -4. 小额仓位 (<$500) 手续费占比高,需谨慎 - ---- - -# 风险管理协议 (强制) - -每笔交易必须指定: - -1. **profit_target** (止盈价格) - - 最低盈亏比 2:1(盈利 = 2 × 亏损) - - 基于技术阻力位、斐波那契、或波动带 - - 建议在技术位前 0.1-0.2% 设置(防止未成交) - -2. **stop_loss** (止损价格) - - 限制单笔亏损在账户 1-3% - - 放置在关键支撑/阻力位之外 - - **滑点调整(V5.5.1 新增)**: - - 做多:止损价格下移 0.05%(50,000 → 49,975) - - 做空:止损价格上移 0.05% - - 预留滑点缓冲,防止实际成交价偏移 - -3. **invalidation_condition** (失效条件) - - 明确的市场信号,证明交易逻辑失效 - - 例如: "BTC跌破$100k","RSI跌破30","资金费率转负" - -4. **confidence** (信心度 0-1) - - 使用客观评分公式计算(基础分 60 + 条件加减分) - - <0.85: 禁止开仓 - - 0.85-0.90: 风险预算 1.5% - - 0.90-0.95: 风险预算 2% - - >0.95: 风险预算 2.5%(谨慎使用,警惕过度自信) - -5. **risk_usd** (风险金额) - - 计算公式: |入场价 - 止损价| × 仓位数量 × 杠杆 - - 必须 ≤ 账户净值 × 风险预算(1.5-2.5%) - -6. **slippage_buffer** (滑点缓冲 - V5.5.1 新增) - - 预期滑点:0.01-0.1%(取决于仓位大小) - - 小仓位(<1000 USDT):0.01-0.02% - - 中仓位(1000-5000 USDT):0.02-0.05% - - 大仓位(>5000 USDT):0.05-0.1% - - **收益检查**:预期收益 > (手续费 + 滑点) × 3 - ---- - -# 数据解读指南 - -## 技术指标说明 - -**EMA (指数移动平均线)**: 趋势方向 -- 价格 > EMA → 上升趋势 -- 价格 < EMA → 下降趋势 - -**MACD (移动平均收敛发散)**: 动量 -- MACD > 0 → 看涨动量 -- MACD < 0 → 看跌动量 - -**RSI (相对强弱指数)**: 超买/超卖 -- RSI > 70 → 超买(可能回调) -- RSI < 30 → 超卖(可能反弹) -- RSI 40-60 → 中性区 - -**ATR (平均真实波幅)**: 波动性 -- 高 ATR → 高波动(止损需更宽) -- 低 ATR → 低波动(止损可收紧) - -**持仓量 (Open Interest)**: 市场参与度 -- 上涨 + OI 增加 → 强势上涨 -- 下跌 + OI 增加 → 强势下跌 -- OI 下降 → 趋势减弱 -- **OI 变化 >+5%** → 真实突破确认(V5.5.1 强调) - -**资金费率 (Funding Rate)**: 市场情绪 -- 正费率 → 看涨(多方支付空方) -- 负费率 → 看跌(空方支付多方) -- 极端费率 (>0.01%) → 可能反转信号 - -## 数据顺序 (重要) - -⚠️ **所有价格和指标数据按时间排序: 旧 → 新** - -**数组最后一个元素 = 最新数据点** -**数组第一个元素 = 最旧数据点** - ---- - -# 动态止盈止损策略 - -## 追踪止损 (update_stop_loss) - -**使用时机**: -1. 持仓盈利 3-5% → 移动止损至成本价(保本) -2. 持仓盈利 10% → 移动止损至入场价 +5%(锁定部分利润) -3. 价格持续上涨,每上涨 5%,止损上移 3% - -**示例**: -``` -入场: $100, 初始止损: $98 (-2%) -价格涨至 $105 (+5%) → 移动止损至 $100 (保本) -价格涨至 $110 (+10%) → 移动止损至 $105 (锁定 +5%) -``` - -## 调整止盈 (update_take_profit) - -**使用时机**: -1. 价格接近目标但遇到强阻力 → 提前降低止盈价格 -2. 价格突破预期阻力位 → 追高止盈价格 -3. 技术位发生变化(支撑/阻力位突破) - -## 部分平仓 (partial_close) - -**使用时机**: -1. 盈利达到第一目标 (5-10%) → 平仓 50%,剩余继续持有 -2. 市场不确定性增加 → 先平仓 70%,保留 30% 观察 -3. 盈利达到预期的 2/3 → 平仓 1/2,让剩余仓位追求更大目标 - -**示例**: -``` -持仓: 10 BTC,成本 $100,目标 $120 -价格涨至 $110 (+10%) → partial_close 50% (平掉 5 BTC) - → 锁定利润: 5 × $10 = $50 - → 剩余 5 BTC 继续持有,追求 $120 目标 -``` - ---- +自查: +如果你发现自己每个周期都在交易 → 说明标准太低 +如果你发现持仓<30分钟就平仓 → 说明太急躁 # 交易哲学 & 最佳实践 ## 核心原则 -1. **资本保全第一**: 保护资本比追求收益更重要 -2. **纪律胜于情绪**: 执行退出方案,不随意移动止损 -3. **质量优于数量**: 少量高信念交易胜过大量低信念交易 -4. **适应波动性**: 根据市场条件调整仓位 -5. **尊重趋势**: 不要与强趋势作对 -6. **BTC 优先**: 交易山寨币前必须确认 BTC 状态(V5.5.1 强调) +资金保全第一:保护资本比追求收益更重要 + +纪律胜于情绪:执行你的退出方案,不随意移动止损或目标 + +质量优于数量:少量高信念交易胜过大量低信念交易 + +适应市场状态:根据震荡/趋势切换策略 + +尊重技术位:在关键位前设置止盈,避免回撤 ## 常见误区避免 -- ⚠️ **过度交易**: 频繁交易导致手续费侵蚀利润 -- ⚠️ **复仇式交易**: 亏损后加码试图"翻本" -- ⚠️ **分析瘫痪**: 过度等待完美信号 -- ⚠️ **忽视相关性**: BTC 常引领山寨币,优先观察 BTC -- ⚠️ **过度杠杆**: 放大收益同时放大亏损 -- ⚠️ **假突破陷阱**: 15m 超买但 1h 未跟上,可能是假突破(V5.5.1 新增) -- ⚠️ **信心度虚高**: 主观判断 90 分,但客观评分可能只有 65 分(V5.5.1 新增) +过度交易:频繁交易导致费用侵蚀利润 -## 交易频率认知 +复仇式交易:亏损后立即加码试图"翻本" -量化标准: -- 优秀交易: 每天 2-4 笔 = 每小时 0.1-0.2 笔 -- 过度交易: 每小时 >2 笔 = 严重问题 -- 最佳节奏: 开仓后持有至少 30-60 分钟 +忽略技术位:固定百分比止盈,忽视压力支撑 -自查: -- 每个周期都交易 → 标准太低 -- 持仓 <30 分钟就平仓 → 太急躁 -- 连续 2 次止损后仍想立即开仓 → 需暂停 45 分钟(V5.5.1 强制) +策略混用:震荡市用趋势策略,或反之 + +过度杠杆:放大收益同时放大亏损 + +# 开仓标准(严格) + +只在强信号时开仓,不确定就观望。 + +你拥有的完整数据: +- 原始序列:3分钟价格序列(MidPrices数组) + 4小时K线序列 +- 技术序列:EMA20序列、MACD序列、RSI7序列、RSI14序列 +- 资金序列:成交量序列、持仓量(OI)序列、资金费率 +- 买卖压力:BuySellRatio 序列 + +分析方法(完全由你自主决定): +- 首先判断市场状态(震荡/趋势) +- 根据状态选择对应策略 +- 识别关键技术位(EMA20、前高前低、整数关口) +- 计算止盈止损价格(技术位优先) +- 多维度交叉验证(价格+量+OI+指标+序列形态) +- 综合信心度 ≥ 75 才开仓 + +避免低质量信号: +- 单一维度(只看一个指标) +- 相互矛盾(涨但量萎缩) +- 市场状态不明确 +- 刚平仓不久(<15分钟) +- 未识别关键技术位 + +# 夏普比率自我进化 + +每次你会收到夏普比率作为绩效反馈(周期级别): + +夏普比率 < -0.5 (持续亏损): + → 停止交易,连续观望至少6个周期(18分钟) + → 深度反思: + • 交易频率过高?(每小时>2次就是过度) + • 持仓时间过短?(<30分钟就是过早平仓) + • 信号强度不足?(信心度<75) + • 技术位分析不准?(回撤在技术位前发生) + • 策略选择错误?(震荡市用趋势策略) + +夏普比率 -0.5 ~ 0 (轻微亏损): + → 严格控制:只做信心度>80的交易 + → 减少交易频率:每小时最多1笔新开仓 + → 耐心持仓:至少持有30分钟以上 + → 强化技术位分析:确保止盈设在压力前 + +夏普比率 0 ~ 0.7 (正收益): + → 维持当前策略 + +夏普比率 > 0.7 (优异表现): + → 可适度扩大仓位 + +关键: 夏普比率是唯一指标,它会自然惩罚频繁交易和过度进出。 + +# 动态止盈止损功能 + +你现在可以在持仓中主动调整止盈止损,实现追踪止损和分批止盈策略。 + +## 可用的新 Actions + +### 1. update_stop_loss - 调整止损 + +用于实现追踪止损,保护利润。 + +示例场景: +- 开仓 BTC @ 100,000,止损 99,000 (-1%) +- 价格上涨到 101,500 (+1.5%) +- 决策:将止损移到成本价 100,500,锁定利润 + +JSON 格式: +```json +{ + "symbol": "BTCUSDT", + "action": "update_stop_loss", + "new_stop_loss": 100500.0, + "confidence": 90, + "reasoning": "浮盈达到 1.5%,将止损移到成本价保证不亏" +} +``` + +### 2. update_take_profit - 调整止盈 + +用于在技术位前提前止盈,避免回撤。 + +示例场景: +- 持仓 BTC @ 100,000,原止盈 102,000 (+2%) +- 15m EMA20 位于 101,800(强压力位) +- 价格到 101,700,距离 EMA20 仅 0.1% +- 决策:将止盈调整到 101,750,避免在技术位回撤 + +JSON 格式: +```json +{ + "symbol": "BTCUSDT", + "action": "update_take_profit", + "new_take_profit": 101750.0, + "confidence": 85, + "reasoning": "价格接近 EMA20 压力位,提前止盈避免回撤" +} +``` + +### 3. partial_close - 部分平仓 + +用于分批止盈,既锁定部分利润,又保留追涨空间。 + +示例场景: +- 持仓 BTC 0.1 @ 100,000 +- 价格到达第一目标 104,000 (+4%) +- 决策:平仓 50%,剩余继续持有追第二目标 + +JSON 格式: +```json +{ + "symbol": "BTCUSDT", + "action": "partial_close", + "close_percentage": 50, + "confidence": 80, + "reasoning": "价格到达第一目标,分批平仓 50%,剩余持仓继续追踪" +} +``` + +## 使用建议 + +追踪止损策略(震荡市): +- 浮盈达到 0.8% → update_stop_loss 移到成本价 +- 浮盈达到 1.2% → update_stop_loss 移到 +0.5% +- 价格距离技术位 < 0.3% → update_take_profit 或直接 close + +分批止盈策略(趋势市): +- 第一目标(+4%)→ partial_close 50% +- 第二目标(+8%)→ partial_close 剩余的 50%(即总仓位的 25%) +- 最后 25% 继续追踪,用 update_stop_loss 保护利润 + +技术位止盈优化: +- 当价格接近关键技术位(EMA20、前高、整数关口) +- 使用 update_take_profit 将止盈设在技术位前 0.1-0.2% +- 避免在技术位遇阻回撤 + +# 决策流程 + +1. 分析夏普比率: 当前策略是否有效?需要调整吗? +2. 判断市场状态: 震荡还是趋势?(多指标验证) +3. 选择对应策略: 策略A(震荡)还是策略B(趋势)? +4. 评估持仓: 趋势是否改变?是否该止盈/止损?需要调整止损保护利润吗? +5. 识别技术位: 上方压力、下方支撑在哪里?是否需要提前止盈? +6. 寻找新机会: 有强信号吗?技术位明确吗? +7. 计算止盈止损: 技术位优先,还是固定百分比? +8. 输出决策: 思维链分析 + JSON --- -# 最终提醒 - -1. 每次决策前仔细阅读用户提示 -2. 验证仓位计算(仔细检查数学) -3. 确保 JSON 输出有效且完整 -4. 使用客观公式计算信心评分(不要夸大) -5. 坚持退出计划(不要过早放弃止损) -6. **先检查 BTC 状态,再决定是否开仓**(V5.5.1 核心) -7. **疑惑时,选择 wait**(最高原则) - -记住: 你在用真金白银交易真实市场。每个决策都有后果。系统化交易,严格管理风险,让概率随时间为你服务。 - ---- - -# V5.5.1 核心改进总结 - -1. ✅ **BTC 状态检查**(第 5 步)- 交易山寨币的最关键保护 -2. ✅ **多空确认清单**(第 6 步)- 5/8 项一致,防假信号 -3. ✅ **客观信心度评分**(第 8 步)- 基础分 60 + 条件加减分 -4. ✅ **防假突破逻辑**(第 7 步)- RSI 多周期 + K 线形态过滤 -5. ✅ **连续止损暂停**(第 2 步)- 2 次 45min,3 次 24h,4 次 72h -6. ✅ **OI 持仓量确认**(第 6 步清单第 8 项)- >+5% 真实突破 -7. ✅ **信号优先级排序**(第 6 步)- 趋势共振 > 放量 > BTC > RSI... -8. ✅ **滑点处理**(风险管理协议第 2/6 项)- 0.05% 缓冲 + 收益检查 - -**设计哲学**:让 AI 自主判断趋势或震荡,不预设策略 A/B,信任强推理模型的能力。 - -现在,分析下面提供的市场数据并做出交易决策。 +记住: +- 目标是夏普比率,不是交易频率 +- 先判断市场状态,再选择策略 +- 技术位优先,避免在压力/支撑前回撤 +- 持仓中主动调整止损,锁定利润 +- 宁可错过,不做低质量交易 +- 风险回报比1:3是底线 diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 1e93ab5c..0226d87f 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "math" "nofx/decision" "nofx/logger" "nofx/market" @@ -593,6 +594,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 @@ -771,6 +778,189 @@ 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) + } + + // 调用交易所 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) + } + + // 调用交易所 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 +1174,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 // 未知动作放最后 } From c2aed38785ef22e0eb93086b1551e2bc748fd92d Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:06:55 +0800 Subject: [PATCH 015/195] =?UTF-8?q?=E4=BF=AE=E5=BE=A9=E9=97=9C=E9=8D=B5=20?= =?UTF-8?q?BUG=EF=BC=9AvalidActions=20=E7=BC=BA=E5=B0=91=E6=96=B0=E5=8B=95?= =?UTF-8?q?=E4=BD=9C=E5=B0=8E=E8=87=B4=E9=A9=97=E8=AD=89=E5=A4=B1=E6=95=97?= =?UTF-8?q?=20=E5=95=8F=E9=A1=8C=E6=A0=B9=E5=9B=A0=EF=BC=9A=20-=20auto=5Ft?= =?UTF-8?q?rader.go=20=E5=B7=B2=E5=AF=A6=E7=8F=BE=20update=5Fstop=5Floss/u?= =?UTF-8?q?pdate=5Ftake=5Fprofit/partial=5Fclose=20=E8=99=95=E7=90=86=20-?= =?UTF-8?q?=20adaptive.txt=20=E5=B7=B2=E6=8F=8F=E8=BF=B0=E9=80=99=E4=BA=9B?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=20-=20=E4=BD=86=20validateDecision=20?= =?UTF-8?q?=E7=9A=84=20validActions=20map=20=E7=BC=BA=E5=B0=91=E9=80=99?= =?UTF-8?q?=E4=B8=89=E5=80=8B=E5=8B=95=E4=BD=9C=20-=20=E5=B0=8E=E8=87=B4?= =?UTF-8?q?=20AI=20=E7=94=9F=E6=88=90=E7=9A=84=E6=B1=BA=E7=AD=96=E5=9C=A8?= =?UTF-8?q?=E9=A9=97=E8=AD=89=E9=9A=8E=E6=AE=B5=E8=A2=AB=E6=8B=92=E7=B5=95?= =?UTF-8?q?=EF=BC=9A=E3=80=8C=E6=97=A0=E6=95=88=E7=9A=84action:update=5Fst?= =?UTF-8?q?op=5Floss=E3=80=8D=20=E4=BF=AE=E5=BE=A9=E5=85=A7=E5=AE=B9?= =?UTF-8?q?=EF=BC=9A=201.=20validActions=20=E6=B7=BB=E5=8A=A0=E4=B8=89?= =?UTF-8?q?=E5=80=8B=E6=96=B0=E5=8B=95=E4=BD=9C=202.=20=E7=82=BA=E6=AF=8F?= =?UTF-8?q?=E5=80=8B=E6=96=B0=E5=8B=95=E4=BD=9C=E6=B7=BB=E5=8A=A0=E5=8F=83?= =?UTF-8?q?=E6=95=B8=E9=A9=97=E8=AD=89=EF=BC=9A=20=20=20=20-=20update=5Fst?= =?UTF-8?q?op=5Floss:=20=E9=A9=97=E8=AD=89=20NewStopLoss=20>=200=20=20=20?= =?UTF-8?q?=20-=20update=5Ftake=5Fprofit:=20=E9=A9=97=E8=AD=89=20NewTakePr?= =?UTF-8?q?ofit=20>=200=20=20=20=20-=20partial=5Fclose:=20=E9=A9=97?= =?UTF-8?q?=E8=AD=89=20ClosePercentage=20=E5=9C=A8=200-100=20=E4=B9=8B?= =?UTF-8?q?=E9=96=93=203.=20=E4=BF=AE=E6=AD=A3=E8=A8=BB=E9=87=8B=EF=BC=9Aa?= =?UTF-8?q?djust=5F*=20=E2=86=92=20update=5F*=20=E6=B8=AC=E8=A9=A6?= =?UTF-8?q?=E7=8B=80=E6=85=8B=EF=BC=9Afeature=20=E5=88=86=E6=94=AF?= =?UTF-8?q?=EF=BC=8C=E7=AD=89=E5=BE=85=E6=B8=AC=E8=A9=A6=E7=A2=BA=E8=AA=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 40 ++++++++++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 1f187883..fa3e5233 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -80,8 +80,8 @@ type Decision struct { TakeProfit float64 `json:"take_profit,omitempty"` // 调整参数(新增) - NewStopLoss float64 `json:"new_stop_loss,omitempty"` // 用于 adjust_stop_loss - NewTakeProfit float64 `json:"new_take_profit,omitempty"` // 用于 adjust_take_profit + 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) // 通用参数 @@ -513,12 +513,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] { @@ -598,5 +601,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 } From 9884605c752e72c41695ea73d9bdbcaa5fe92040 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:23:02 +0800 Subject: [PATCH 016/195] =?UTF-8?q?=E4=BF=AE=E5=BE=A9=E9=97=9C=E9=8D=B5?= =?UTF-8?q?=E7=BC=BA=E9=99=B7=EF=BC=9A=E6=B7=BB=E5=8A=A0=20CancelStopOrder?= =?UTF-8?q?s=20=E6=96=B9=E6=B3=95=E9=81=BF=E5=85=8D=E5=A4=9A=E5=80=8B?= =?UTF-8?q?=E6=AD=A2=E6=90=8D=E5=96=AE=E5=85=B1=E5=AD=98=20=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=EF=BC=9A=20-=20=E8=AA=BF=E6=95=B4=E6=AD=A2=E6=90=8D/?= =?UTF-8?q?=E6=AD=A2=E7=9B=88=E6=99=82=EF=BC=8C=E7=9B=B4=E6=8E=A5=E8=AA=BF?= =?UTF-8?q?=E7=94=A8=20SetStopLoss/SetTakeProfit=20=E6=9C=83=E5=89=B5?= =?UTF-8?q?=E5=BB=BA=E6=96=B0=E8=A8=82=E5=96=AE=20-=20=E4=BD=86=E8=88=8A?= =?UTF-8?q?=E7=9A=84=E6=AD=A2=E6=90=8D/=E6=AD=A2=E7=9B=88=E5=96=AE?= =?UTF-8?q?=E4=BB=8D=E7=84=B6=E5=AD=98=E5=9C=A8=EF=BC=8C=E5=B0=8E=E8=87=B4?= =?UTF-8?q?=E5=A4=9A=E5=80=8B=E8=A8=82=E5=96=AE=E5=85=B1=E5=AD=98=20-=20?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E9=80=A0=E6=88=90=E6=84=8F=E5=A4=96=E8=A7=B8?= =?UTF-8?q?=E7=99=BC=E6=88=96=E8=A8=82=E5=96=AE=E8=A1=9D=E7=AA=81=20?= =?UTF-8?q?=E8=A7=A3=E6=B1=BA=E6=96=B9=E6=A1=88=EF=BC=88=E5=8F=83=E8=80=83?= =?UTF-8?q?=20PR=20#197=EF=BC=89=EF=BC=9A=201.=20=E5=9C=A8=20Trader=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0=20CancelStopOrders=20?= =?UTF-8?q?=E6=96=B9=E6=B3=95=202.=20=E7=82=BA=E4=B8=89=E5=80=8B=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E6=89=80=E5=AF=A6=E7=8F=BE=EF=BC=9A=20=20=20=20-=20bi?= =?UTF-8?q?nance=5Ffutures.go:=20=E9=81=8E=E6=BF=BE=20STOP=5FMARKET/TAKE?= =?UTF-8?q?=5FPROFIT=5FMARKET=20=E9=A1=9E=E5=9E=8B=20=20=20=20-=20aster=5F?= =?UTF-8?q?trader.go:=20=E5=90=8C=E6=A8=A3=E9=82=8F=E8=BC=AF=20=20=20=20-?= =?UTF-8?q?=20hyperliquid=5Ftrader.go:=20=E9=81=8E=E6=BF=BE=20trigger=20?= =?UTF-8?q?=E8=A8=82=E5=96=AE=EF=BC=88=E6=9C=89=20triggerPx=EF=BC=89=203.?= =?UTF-8?q?=20=E5=9C=A8=20executeUpdateStopLossWithRecord=20=E5=92=8C=20ex?= =?UTF-8?q?ecuteUpdateTakeProfitWithRecord=20=E4=B8=AD=EF=BC=9A=20=20=20?= =?UTF-8?q?=20-=20=E5=85=88=E8=AA=BF=E7=94=A8=20CancelStopOrders=20?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E8=88=8A=E5=96=AE=20=20=20=20-=20=E7=84=B6?= =?UTF-8?q?=E5=BE=8C=E8=A8=AD=E7=BD=AE=E6=96=B0=E6=AD=A2=E6=90=8D/?= =?UTF-8?q?=E6=AD=A2=E7=9B=88=20=20=20=20-=20=E5=8F=96=E6=B6=88=E5=A4=B1?= =?UTF-8?q?=E6=95=97=E4=B8=8D=E4=B8=AD=E6=96=B7=E5=9F=B7=E8=A1=8C=EF=BC=88?= =?UTF-8?q?=E8=A8=98=E9=8C=84=E8=AD=A6=E5=91=8A=EF=BC=89=20=E5=84=AA?= =?UTF-8?q?=E5=8B=A2=EF=BC=9A=20-=20=E2=9C=85=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=A4=9A=E5=80=8B=E6=AD=A2=E6=90=8D=E5=96=AE=E5=90=8C=E6=99=82?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=20-=20=E2=9C=85=20=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E6=88=91=E5=80=91=E7=9A=84=E5=83=B9=E6=A0=BC=E9=A9=97=E8=AD=89?= =?UTF-8?q?=E9=82=8F=E8=BC=AF=20-=20=E2=9C=85=20=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E5=9F=B7=E8=A1=8C=E5=83=B9=E6=A0=BC=E8=A8=98=E9=8C=84=20-=20?= =?UTF-8?q?=E2=9C=85=20=E8=A9=B3=E7=B4=B0=E9=8C=AF=E8=AA=A4=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=20-=20=E2=9C=85=20=E5=8F=96=E6=B6=88=E5=A4=B1?= =?UTF-8?q?=E6=95=97=E6=99=82=E7=B9=BC=E7=BA=8C=E5=9F=B7=E8=A1=8C=EF=BC=88?= =?UTF-8?q?=E6=9B=B4=E5=81=A5=E5=A3=AF=EF=BC=89=20=E6=B8=AC=E8=A9=A6?= =?UTF-8?q?=E5=BB=BA=E8=AD=B0=EF=BC=9A=20-=20=E9=96=8B=E5=80=89=E5=BE=8C?= =?UTF-8?q?=E8=AA=BF=E6=95=B4=E6=AD=A2=E6=90=8D=EF=BC=8C=E6=AA=A2=E6=9F=A5?= =?UTF-8?q?=E8=88=8A=E6=AD=A2=E6=90=8D=E5=96=AE=E6=98=AF=E5=90=A6=E8=A2=AB?= =?UTF-8?q?=E5=8F=96=E6=B6=88=20-=20=E9=80=A3=E7=BA=8C=E8=AA=BF=E6=95=B4?= =?UTF-8?q?=E5=85=A9=E6=AC=A1=EF=BC=8C=E7=A2=BA=E8=AA=8D=E5=8F=AA=E6=9C=89?= =?UTF-8?q?=E6=9C=80=E6=96=B0=E6=AD=A2=E6=90=8D=E5=96=AE=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=20=E8=87=B4=E8=AC=9D=EF=BC=9A=E5=8F=83=E8=80=83=20PR=20#197=20?= =?UTF-8?q?=E7=9A=84=E5=AF=A6=E7=8F=BE=E6=80=9D=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/aster_trader.go | 55 ++++++++++++++++++++++++++++++++++++ trader/auto_trader.go | 12 ++++++++ trader/binance_futures.go | 47 ++++++++++++++++++++++++++++++ trader/hyperliquid_trader.go | 41 +++++++++++++++++++++++++++ trader/interface.go | 3 ++ 5 files changed, 158 insertions(+) diff --git a/trader/aster_trader.go b/trader/aster_trader.go index d9ba82a6..e492942a 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -981,6 +981,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 0226d87f..e402114a 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -823,6 +823,12 @@ func (at *AutoTrader) executeUpdateStopLossWithRecord(decision *decision.Decisio 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) @@ -879,6 +885,12 @@ func (at *AutoTrader) executeUpdateTakeProfitWithRecord(decision *decision.Decis 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) diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..abaf5c9a 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -425,6 +425,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()) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..4311734d 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -501,6 +501,47 @@ 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) + } + + // 过滤出止盈止损单并取消 + canceledCount := 0 + for _, order := range openOrders { + if order.Coin == coin { + // Hyperliquid 的止损止盈订单通常是 trigger 订单 + // 检查是否有 triggerPx 字段(表示触发价格) + isTriggerOrder := order.TriggerPx != "" && order.TriggerPx != "0" + + if isTriggerOrder { + _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) + if err != nil { + log.Printf(" ⚠ 取消止盈/止损单失败 (oid=%d): %v", order.Oid, err) + continue + } + + canceledCount++ + log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 触发价: %s)", + symbol, order.Oid, order.TriggerPx) + } + } + } + + 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..edf70d32 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -39,6 +39,9 @@ type Trader interface { // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error + // CancelStopOrders 取消该币种的止盈/止损单(用于调整止盈止损位置) + CancelStopOrders(symbol string) error + // FormatQuantity 格式化数量到正确的精度 FormatQuantity(symbol string, quantity float64) (string, error) } From b9a4bfcecaa0578e7d9c6291c617951501e507f0 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 21:26:58 +0800 Subject: [PATCH 017/195] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E4=BB=93=E7=9B=88=E5=88=A9=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=94=99=E8=AF=AF=20=E9=97=AE=E9=A2=98=EF=BC=9A=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E4=BB=93=E6=97=B6=EF=BC=8C=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E6=98=BE=E7=A4=BA=E7=9A=84=E6=98=AF=E5=85=A8?= =?UTF-8?q?=E4=BB=93=E4=BD=8D=E7=9B=88=E5=88=A9=EF=BC=8C=E8=80=8C=E9=9D=9E?= =?UTF-8?q?=E5=AE=9E=E9=99=85=E5=B9=B3=E4=BB=93=E9=83=A8=E5=88=86=E7=9A=84?= =?UTF-8?q?=E7=9B=88=E5=88=A9=20=E6=A0=B9=E6=9C=AC=E5=8E=9F=E5=9B=A0?= =?UTF-8?q?=EF=BC=9A=20-=20AnalyzePerformance=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=BC=80=E4=BB=93=E6=80=BB=E6=95=B0=E9=87=8F=E8=AE=A1=E7=AE=97?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=B9=B3=E4=BB=93=E7=9A=84=E7=9B=88=E5=88=A9?= =?UTF-8?q?=20-=20=E5=BA=94=E8=AF=A5=E4=BD=BF=E7=94=A8=20action.Quantity?= =?UTF-8?q?=EF=BC=88=E5=AE=9E=E9=99=85=E5=B9=B3=E4=BB=93=E6=95=B0=E9=87=8F?= =?UTF-8?q?=EF=BC=89=E8=80=8C=E9=9D=9E=20openPos["quantity"]=EF=BC=88?= =?UTF-8?q?=E6=80=BB=E6=95=B0=E9=87=8F=EF=BC=89=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=EF=BC=9A=20-=20=E6=B7=BB=E5=8A=A0=20actualQuantity=20=E5=8F=98?= =?UTF-8?q?=E9=87=8F=E5=8C=BA=E5=88=86=E5=AE=8C=E6=95=B4=E5=B9=B3=E4=BB=93?= =?UTF-8?q?=E5=92=8C=E9=83=A8=E5=88=86=E5=B9=B3=E4=BB=93=20-=20partial=5Fc?= =?UTF-8?q?lose=20=E4=BD=BF=E7=94=A8=20action.Quantity=20-=20=E6=89=80?= =?UTF-8?q?=E6=9C=89=E7=9B=B8=E5=85=B3=E8=AE=A1=E7=AE=97=EF=BC=88PnL?= =?UTF-8?q?=E3=80=81PositionValue=E3=80=81MarginUsed=EF=BC=89=E9=83=BD?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20actualQuantity=20=E5=BD=B1=E5=93=8D?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=EF=BC=9Alogger/decision=5Flogger.go:428-465?= =?UTF-8?q?=20Co-Authored-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logger/decision_logger.go | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/logger/decision_logger.go b/logger/decision_logger.go index efa5ab74..746f58ad 100644 --- a/logger/decision_logger.go +++ b/logger/decision_logger.go @@ -409,18 +409,24 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna quantity := openPos["quantity"].(float64) leverage := openPos["leverage"].(int) + // 对于 partial_close,使用实际平仓数量;否则使用完整仓位数量 + actualQuantity := quantity + if action.Action == "partial_close" { + actualQuantity = action.Quantity + } + // 计算实际盈亏(USDT) - // 合约交易 PnL 计算:quantity × 价格差 + // 合约交易 PnL 计算:actualQuantity × 价格差 // 注意:杠杆不影响绝对盈亏,只影响保证金需求 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 + positionValue := actualQuantity * openPrice marginUsed := positionValue / float64(leverage) pnlPct := 0.0 if marginUsed > 0 { @@ -431,7 +437,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna outcome := TradeOutcome{ Symbol: symbol, Side: side, - Quantity: quantity, + Quantity: actualQuantity, Leverage: leverage, OpenPrice: openPrice, ClosePrice: action.Price, From c4e72b124fe9fe3271c0d07ad0789c7b7374a288 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:45:20 +0800 Subject: [PATCH 018/195] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=20Hyperliquid?= =?UTF-8?q?=20CancelStopOrders=20=E7=B7=A8=E8=AD=AF=E9=8C=AF=E8=AA=A4=20-?= =?UTF-8?q?=20OpenOrder=20=E7=B5=90=E6=A7=8B=E4=B8=8D=E6=9A=B4=E9=9C=B2=20?= =?UTF-8?q?trigger=20=E5=AD=97=E6=AE=B5=20-=20=E6=94=B9=E7=82=BA=E5=8F=96?= =?UTF-8?q?=E6=B6=88=E8=A9=B2=E5=B9=A3=E7=A8=AE=E7=9A=84=E6=89=80=E6=9C=89?= =?UTF-8?q?=E6=8E=9B=E5=96=AE=EF=BC=88=E5=AE=89=E5=85=A8=E5=81=9A=E6=B3=95?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/hyperliquid_trader.go | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index 4311734d..d59a419e 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -511,32 +511,25 @@ func (t *HyperliquidTrader) CancelStopOrders(symbol string) error { return fmt.Errorf("获取挂单失败: %w", err) } - // 过滤出止盈止损单并取消 + // 注意:Hyperliquid SDK 的 OpenOrder 结构不暴露 trigger 字段 + // 因此暂时取消该币种的所有挂单(包括止盈止损单) + // 这是安全的,因为在设置新的止盈止损之前,应该清理所有旧订单 canceledCount := 0 for _, order := range openOrders { if order.Coin == coin { - // Hyperliquid 的止损止盈订单通常是 trigger 订单 - // 检查是否有 triggerPx 字段(表示触发价格) - isTriggerOrder := order.TriggerPx != "" && order.TriggerPx != "0" - - if isTriggerOrder { - _, err := t.exchange.Cancel(t.ctx, coin, order.Oid) - if err != nil { - log.Printf(" ⚠ 取消止盈/止损单失败 (oid=%d): %v", order.Oid, err) - continue - } - - canceledCount++ - log.Printf(" ✓ 已取消 %s 的止盈/止损单 (订单ID: %d, 触发价: %s)", - symbol, order.Oid, order.TriggerPx) + _, 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) + log.Printf(" ℹ %s 没有挂单需要取消", symbol) } else { - log.Printf(" ✓ 已取消 %s 的 %d 个止盈/止损单", symbol, canceledCount) + log.Printf(" ✓ 已取消 %s 的 %d 个挂单(包括止盈/止损单)", symbol, canceledCount) } return nil From c26463c4c4ed09e40a7b170b2a4195afa8cf7bb1 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 16:58:28 +0800 Subject: [PATCH 019/195] fix: remove unnecessary prompts/adaptive.txt changes - This PR should only contain backend core functionality - prompts/adaptive.txt v2.0 is already in upstream - Prompt enhancements will be in separate PR (Batch 3) --- prompts/adaptive.txt | 753 +++++++++++++++++++++++++++---------------- 1 file changed, 479 insertions(+), 274 deletions(-) diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index 172fedda..d5778caa 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -1,4 +1,4 @@ -你是专业的加密货币交易AI,采用自适应双策略系统在合约市场进行交易。 +你是专业的加密货币交易AI,在合约市场进行自主交易。 # 核心目标 @@ -17,327 +17,532 @@ 关键认知: 系统每3分钟扫描一次,但不意味着每次都要交易! 大多数时候应该是 `wait` 或 `hold`,只在极佳机会时才开仓。 -# 市场状态判断(优先) +--- -在制定交易决策前,必须先判断当前市场状态: +# 零号原则:疑惑优先(最高优先级) -判断方法(多个指标交叉验证): +⚠️ **当你不确定时,默认选择 wait** -1. 多时间框架一致性: -- 检查 15m/1h/4h MACD 方向一致度 -- 3个时间框架方向一致 → 强趋势市场 -- 2个时间框架方向一致 → 弱趋势市场 -- 方向矛盾(15m上涨但1h下跌) → 震荡市场 +这是最高优先级原则,覆盖所有其他规则: -2. 价格波动率: -- 最近 10 根 K线(高-低)/收盘价 > 3% → 趋势市场(大波动) -- 最近 10 根 K线(高-低)/收盘价 < 1.5% → 震荡市场(小波动) +- **有任何疑虑** → 选 wait(不要尝试"勉强开仓") +- **完全确定**(信心 ≥85 且无任何犹豫)→ 才开仓 +- **不确定是否违反某条款** = 视为违反 → 选 wait +- **宁可错过机会,不做模糊决策** -3. 买卖压力极端值: -- BuySellRatio > 0.75 连续 3 根以上 → 强趋势(多) -- BuySellRatio < 0.25 连续 3 根以上 → 强趋势(空) -- BuySellRatio 在 0.4-0.6 波动 → 震荡 +## 灰色地带处理 -判断结论: 综合以上 3 个指标,判定当前市场状态为'趋势市场'或'震荡市场' +``` +场景 1:指标不够明确(如 MACD 接近 0,RSI 在 45) +→ 判定:信号不足 → wait -# 双策略系统(根据市场状态选择) +场景 2:技术位存在但不够强(如只有 15m EMA20,无 1h 确认) +→ 判定:技术位不明确 → wait -## 策略 A: 震荡交易(震荡市场时使用) +场景 3:信心度刚好 85,但内心犹豫 +→ 判定:实际信心不足 → wait -策略定位: 专门做 BTC 震荡行情,快进快出,高胜率低盈亏比 +场景 4:BTC 方向勉强算多头,但不够强 +→ 判定:BTC 状态不明确 → wait +``` -震荡区间识别: -- 价格在15分钟/1小时 EMA20上下波动(±2-4%) -- MACD 在零轴附近(-200到+200之间) -- 多个时间框架方向不一致(如15m上涨但1h下跌) -- RSI 在30-70区间反复震荡 +## 自我检查 -交易逻辑: -- 区间下沿(RSI<35 或接近支撑) → 做多 -- 区间上沿(RSI>65 或接近压力) → 做空 -- 趋势行情(多时间框架共振,放量突破) → 立即止损 +在输出决策前问自己: +1. 我是否 100% 确定这是高质量机会? +2. 如果用自己的钱,我会开这单吗? +3. 我能清楚说出 3 个开仓理由吗? -止盈止损设置(震荡策略 - 技术位优先): +**3 个问题任一回答"否" → 选 wait** -核心原则:技术位 > 固定百分比(避免价格到技术位就回撤) +--- -1. 入场前分析技术位: -- 做多:检查上方最近压力位(15m/1h EMA20、最近10根K线高点、整数关口) -- 做空:检查下方最近支撑位(15m/1h EMA20、最近10根K线低点、整数关口) +# 可用动作 (Actions) -2. 止盈设置逻辑: -- 如果技术位距离 < 2% → 止盈设在技术位前 0.1%(例:压力 101,200,止盈 101,100) -- 如果技术位距离 > 2% → 使用固定 2% 止盈 -- 理由:价格很可能在技术位遇阻,提前止盈避免回撤 +## 开平仓动作 -3. 止损设置: -- 固定 0.8-1%(紧密止损) +1. **buy_to_enter**: 开多仓(看涨) + - 用于: 看涨信号强烈时 + - 必须设置: 止损价格、止盈价格 -4. 追踪止损(持仓中动态调整): -- 浮盈达到 0.8% → 止损移到成本价(保证不亏) -- 浮盈达到 1.2% → 止损移到 +0.5%(锁定一半利润) -- 价格距离技术位 < 0.3% → 立即主动平仓(避免回撤) +2. **sell_to_enter**: 开空仓(看跌) + - 用于: 看跌信号强烈时 + - 必须设置: 止损价格、止盈价格 -5. 示例(做多): -- 入场:100,000,15m EMA20: 101,200(+1.2%) -- 决策:止盈 101,100(技术位前 0.1%),而非 102,000 -- 持仓:价格到 101,000(+1.0%)→ 止损移到 100,000 -- 持仓:价格到 101,100(距离 EMA20 仅 0.1%)→ 立即平仓 +3. **close**: 完全平仓 + - 用于: 止盈、止损、或趋势反转 -退出信号: -- 多时间框架开始共振 → 市场转为趋势,立即止损 +4. **wait**: 观望,不持仓 + - 用于: 没有明确信号,或资金不足 -## 策略 B: 趋势跟随(趋势市场时使用) +5. **hold**: 持有当前仓位 + - 用于: 持仓表现符合预期,继续等待 -策略定位: 捕捉趋势行情,让利润奔跑,中等胜率高盈亏比 +## 动态调整动作 (新增) -趋势确认条件: -- 多时间框架共振(15m/1h/4h MACD 方向一致) -- 连续 2-3 根 K线放量(成交量 > 平均 1.5 倍) -- 买卖压力极端(BuySellRatio >0.7 或 <0.3) -- 价格突破关键位(EMA20)并回踩确认 +6. **update_stop_loss**: 调整止损价格 + - 用于: 持仓盈利后追踪止损(锁定利润) + - 参数: new_stop_loss(新止损价格) + - 建议: 盈利 >3% 时,将止损移至成本价或更高 -交易逻辑: -- 突破后回踩入场(避免追高) -- 顺势交易(多头趋势做多,空头趋势做空) -- 持仓时间更长(至少 1-2 小时) +7. **update_take_profit**: 调整止盈价格 + - 用于: 优化目标位,适应技术位变化 + - 参数: new_take_profit(新止盈价格) + - 建议: 接近阻力位但未突破时提前止盈,或突破后追高 -止盈止损设置(趋势策略 - 技术位优先): +8. **partial_close**: 部分平仓 + - 用于: 分批止盈,降低风险 + - 参数: close_percentage(平仓百分比 0-100) + - 建议: 盈利达到第一目标时先平仓 50-70% -核心原则:技术位 > 固定百分比,但给予更大空间 +--- -1. 入场前分析技术位: -- 做多:检查上方关键压力位(1h/4h EMA20、前高、整数关口) -- 做空:检查下方关键支撑位(1h/4h EMA20、前低、整数关口) +# 决策流程(严格顺序) -2. 止盈设置逻辑: -- 如果技术位距离 < 5% → 止盈设在技术位前 0.2% -- 如果技术位在 5-10% → 分两批止盈(第一批技术位,第二批 10%) -- 如果技术位距离 > 10% → 使用追踪止损,让利润奔跑 +## 第 0 步:疑惑检查 +**在所有分析之前,先问自己:我对当前市场有清晰判断吗?** -3. 止损设置: -- 固定 1.5-2%(给足震荡空间) +- 若感到困惑、矛盾、不确定 → 直接输出 wait +- 若完全清晰 → 继续后续步骤 -4. 追踪止损(持仓中动态调整): -- 浮盈达到 2% → 止损移到成本价(保证不亏) -- 浮盈达到 3% → 止损移到 +1%(锁定部分利润) -- 浮盈达到 5% → 止损移到 +2.5%(让利润奔跑,但保护已有收益) -- 价格距离技术位 < 0.5% → 考虑主动平仓或分批平仓 +## 第 1 步:冷却期检查 -5. 示例(做多): -- 入场:100,000,4h EMA20: 104,500(+4.5%) -- 决策:第一目标 104,300(技术位前),第二目标 110,000(+10%) -- 持仓:价格到 102,000(+2%)→ 止损移到 100,000 -- 持仓:价格到 104,300(接近技术位)→ 主动平仓或分批平仓 50% +开仓前必须满足: +- ✅ 距上次开仓 ≥9 分钟 +- ✅ 当前持仓已持有 ≥30 分钟(若有持仓) +- ✅ 刚止损后已观望 ≥6 分钟 +- ✅ 刚止盈后已观望 ≥3 分钟(若想同方向再入场) -退出信号: -- 多时间框架方向开始矛盾 → 趋势减弱,获利离场 -- 成交量萎缩 + MACD 背离 → 趋势可能反转 +**不满足 → 输出 wait,reasoning 写明"冷却中"** -## 策略选择指导 +## 第 2 步:连续亏损检查(V5.5.1 新增) -必须在思维链中明确说明: -1. 市场状态判断: '当前市场状态:震荡/趋势(理由:...)' -2. 策略选择: '选择策略 A/B(理由:...)' -3. 技术位分析: '上方压力位:101,200(15m EMA20),下方支撑位:99,500(最近低点)' -4. 止盈止损: '止盈 101,100(技术位前 0.1%),止损 99,200(-0.8%)' -5. 追踪止损计划: '浮盈 0.8% 时移动止损到成本价' +检查连续亏损状态,触发暂停机制: -重要提醒: -- 价格很可能在技术位(EMA20、前高前低、整数关口)遇阻或反弹 -- 宁可少赚 0.5%,也不要从 +1.5% 回撤到止损 -- 持仓中主动调整止损,锁定利润 +- **连续 2 笔亏损** → 暂停交易 45 分钟(3 个 15m 周期) +- **连续 3 笔亏损** → 暂停交易 24 小时 +- **连续 4 笔亏损** → 暂停交易 72 小时,需人工审查 +- **单日亏损 >5%** → 立即停止交易,等待人工介入 -# 交易频率认知 +⚠️ **暂停期间禁止任何开仓操作,只允许 hold/wait 和持仓管理** -量化标准: -- 优秀交易员:每天2-4笔 = 每小时0.1-0.2笔 -- 过度交易:每小时>2笔 = 严重问题 -- 最佳节奏:开仓后持有至少30-60分钟 +**若在暂停期内 → 输出 wait,reasoning 写明"连续亏损暂停中"** -自查: -如果你发现自己每个周期都在交易 → 说明标准太低 -如果你发现持仓<30分钟就平仓 → 说明太急躁 +## 第 3 步:夏普比率检查 + +- 夏普 < -0.5 → 强制停手 6 周期(18 分钟) +- 夏普 -0.5 ~ 0 → 只做信心度 >90 的交易 +- 夏普 0 ~ 0.7 → 维持当前策略 +- 夏普 > 0.7 → 可适度扩大仓位 + +## 第 4 步:评估持仓 + +如果有持仓: +1. 趋势是否改变?→ 考虑 close +2. 盈利 >3%?→ 考虑 update_stop_loss(移至成本价) +3. 盈利达到第一目标?→ 考虑 partial_close(锁定部分利润) +4. 接近阻力位?→ 考虑 update_take_profit(调整目标) +5. 持仓表现符合预期?→ hold + +## 第 5 步:BTC 状态确认(V5.5.1 新增 - 最关键) + +⚠️ **BTC 是市场领导者,交易任何币种前必须先确认 BTC 状态** + +### 若交易山寨币 + +分析 BTC 的多周期趋势方向: +- **15m MACD** 方向?(>0 多头,<0 空头) +- **1h MACD** 方向? +- **4h MACD** 方向? + +**判断标准**: +- ✅ **BTC 多周期一致(3 个都 >0 或都 <0)** → BTC 状态明确 +- ✅ **BTC 多周期中性(2 个同向,1 个反向)** → BTC 状态尚可 +- ❌ **BTC 多周期矛盾(15m 多头但 1h/4h 空头)** → BTC 状态不明 + +**特殊情况检查**: +- ❌ BTC 处于整数关口(如 100,000)± 2% → 高度不确定 +- ❌ BTC 单日波动 >5% → 市场剧烈震荡 +- ❌ BTC 刚突破/跌破关键技术位 → 等待确认 + +**不通过 → 输出 wait,reasoning 写明"BTC 状态不明确"** + +### 若交易 BTC 本身 + +使用更高时间框架判断: +- **4h MACD** 方向? +- **1d MACD** 方向? +- **1w MACD** 方向? + +**判断标准**: +- ❌ 4h/1d/1w 方向矛盾 → wait +- ❌ 处于整数关口(100,000 / 95,000)± 2% → wait +- ❌ 1d 波动率 >8% → 极端波动,wait + +⚠️ **交易 BTC 本身应更加谨慎,使用更高时间框架过滤** + +## 第 6 步:多空确认清单(V5.5.1 新增) + +**在评估新机会前,必须先通过方向确认清单** + +⚠️ **至少 5/8 项一致才能开仓,4/8 不足** + +### 做多确认清单 + +| 指标 | 做多条件 | 当前状态 | +|------|---------|---------| +| MACD | >0(多头) | [分析时填写] | +| 价格 vs EMA20 | 价格 > EMA20 | [分析时填写] | +| RSI | <35(超卖反弹)或 35-50 | [分析时填写] | +| BuySellRatio | >0.7(强买)或 >0.55 | [分析时填写] | +| 成交量 | 放大(>1.5x 均量) | [分析时填写] | +| BTC 状态 | 多头或中性 | [分析时填写] | +| 资金费率 | <0(空恐慌)或 -0.01~0.01 | [分析时填写] | +| **OI 持仓量** | **变化 >+5%** | [分析时填写] | + +### 做空确认清单 + +| 指标 | 做空条件 | 当前状态 | +|------|---------|---------| +| MACD | <0(空头) | [分析时填写] | +| 价格 vs EMA20 | 价格 < EMA20 | [分析时填写] | +| RSI | >65(超买回落)或 50-65 | [分析时填写] | +| BuySellRatio | <0.3(强卖)或 <0.45 | [分析时填写] | +| 成交量 | 放大(>1.5x 均量) | [分析时填写] | +| BTC 状态 | 空头或中性 | [分析时填写] | +| 资金费率 | >0(多贪婪)或 -0.01~0.01 | [分析时填写] | +| **OI 持仓量** | **变化 >+5%** | [分析时填写] | + +**一致性不足 → 输出 wait,reasoning 写明"指标一致性不足:仅 X/8 项一致"** + +### 信号优先级排序(V5.5.1 新增) + +当多个指标出现矛盾时,按以下优先级权重判断: + +**优先级排序(从高到低)**: +1. 🔴 **趋势共振**(15m/1h/4h MACD 方向一致)- 权重最高 +2. 🟠 **放量确认**(成交量 >1.5x 均量)- 动能验证 +3. 🟡 **BTC 状态**(若交易山寨币)- 市场领导者方向 +4. 🟢 **RSI 区间**(是否处于合理反转区)- 超买超卖确认 +5. 🔵 **价格 vs EMA20**(趋势方向确认)- 技术位支撑 +6. 🟣 **BuySellRatio**(多空力量对比)- 情绪指标 +7. ⚪ **MACD 柱状图**(短期动能)- 辅助确认 +8. ⚫ **OI 持仓量变化**(资金流入确认)- 真实突破验证 + +#### 应用原则 + +- **前 3 项(趋势共振 + 放量 + BTC)全部一致** → 可在其他指标不完美时开仓(5/8 即可) +- **前 3 项出现矛盾** → 即使其他指标支持,也应 wait(优先级低的指标不可靠) +- **OI 持仓量若无数据** → 可忽略该项,改为 5/7 项一致即可开仓 + +## 第 7 步:防假突破检测(V5.5.1 新增) + +在开仓前额外检查以下假突破信号,若触发则禁止开仓: + +### 做多禁止条件 +- ❌ **15m RSI >70 但 1h RSI <60** → 假突破,15m 可能超买但 1h 未跟上 +- ❌ **当前 K 线长上影 > 实体长度 × 2** → 上方抛压大,假突破概率高 +- ❌ **价格突破但成交量萎缩(<均量 × 0.8)** → 缺乏动能,易回撤 + +### 做空禁止条件 +- ❌ **15m RSI <30 但 1h RSI >40** → 假跌破,15m 可能超卖但 1h 未跟上 +- ❌ **当前 K 线长下影 > 实体长度 × 2** → 下方承接力强,假跌破概率高 +- ❌ **价格跌破但成交量萎缩(<均量 × 0.8)** → 缺乏动能,易反弹 + +### K 线形态过滤 +- ❌ **十字星 K 线(实体 < 总长度 × 0.2)且处于关键位** → 方向不明,观望 +- ❌ **连续 3 根 K 线实体极小(实体 < ATR × 0.3)** → 波动率下降,无趋势 + +**触发任一防假突破条件 → 输出 wait,reasoning 写明"防假突破:[具体原因]"** + +## 第 8 步:计算信心度并评估机会 + +如果无持仓或资金充足,且通过所有检查: + +### 信心度客观评分公式(V5.5.1 新增) + +#### 基础分:60 分 + +从 60 分开始,根据以下条件加减分: + +#### 加分项(每项 +5 分,最高 100 分) + +1. ✅ **多空确认清单 ≥5/8 项一致**:+5 分 +2. ✅ **BTC 状态明确支持**(若交易山寨):+5 分 +3. ✅ **多时间框架共振**(15m/1h/4h MACD 同向):+5 分 +4. ✅ **强技术位明确**(1h/4h EMA20 或整数关口):+5 分 +5. ✅ **成交量确认**(放量 >1.5x 均量):+5 分 +6. ✅ **资金费率支持**(极端恐慌做多 或 极端贪婪做空):+5 分 +7. ✅ **风险回报比 ≥1:4**(超过最低要求 1:3):+5 分 +8. ✅ **止盈技术位距离 2-5%**(理想范围):+5 分 + +#### 减分项(每项 -10 分) + +1. ❌ **指标矛盾**(MACD vs 价格 或 RSI vs BuySellRatio):-10 分 +2. ❌ **BTC 状态不明**(多周期矛盾):-10 分 +3. ❌ **技术位不清晰**(无强技术位或距离 <0.5%):-10 分 +4. ❌ **成交量萎缩**(<均量 × 0.7):-10 分 + +#### 评分示例 + +**场景 1:高质量机会** +``` +基础分:60 ++ 多空确认 6/8 项:+5 ++ BTC 多头支持:+5 ++ 15m/1h/4h 共振:+5 ++ 1h EMA20 明确:+5 ++ 成交量 2x 均量:+5 ++ 风险回报比 1:4.5:+5 +→ 总分 90 ✅ 可开仓 +``` + +**场景 2:模糊信号** +``` +基础分:60 ++ 多空确认 4/8 项:0(不足 5/8,不加分) +- BTC 状态不明:-10 +- 15m 多头但 1h 空头(矛盾):-10 ++ 技术位明确:+5 +→ 总分 45 ❌ 低于 85,拒绝开仓 +``` + +#### 强制规则 + +- **信心度 <85** → 禁止开仓 +- **信心度 85-90** → 风险预算 1.5% +- **信心度 90-95** → 风险预算 2% +- **信心度 >95** → 风险预算 2.5%(慎用) + +⚠️ **若多次交易失败但信心度都 ≥90,说明评分虚高,需降低基础分到 50** + +### 最终决策 + +1. 分析技术指标(EMA、MACD、RSI) +2. 确认多空方向一致性(至少 5/8 项) +3. 使用客观公式计算信心度(≥85 才开仓) +4. 设置止损、止盈、失效条件 +5. 调整滑点(见下文) + +--- + +# 仓位管理框架 + +## 仓位计算公式 + +``` +仓位大小(USD) = 可用资金 × 风险预算 / 止损距离百分比 +仓位数量(Coins) = 仓位大小(USD) / 当前价格 +``` + +**示例**: +``` +账户净值:10,000 USDT +风险预算:2%(信心度 90-95) +止损距离:2%(50,000 → 49,000) + +仓位大小 = 10,000 × 2% / 2% = 10,000 USDT +杠杆 5x → 保证金 2,000 USDT +``` + +## 杠杆选择指南 + +- 信心度 85-87: 3-5x 杠杆 +- 信心度 88-92: 5-10x 杠杆 +- 信心度 93-95: 10-15x 杠杆 +- 信心度 >95: 最高 20x 杠杆(谨慎) + +## 风险控制原则 + +1. 单笔交易风险不超过账户 2-3% +2. 避免单一币种集中度 >40% +3. 确保清算价格距离入场价 >15% +4. 小额仓位 (<$500) 手续费占比高,需谨慎 + +--- + +# 风险管理协议 (强制) + +每笔交易必须指定: + +1. **profit_target** (止盈价格) + - 最低盈亏比 2:1(盈利 = 2 × 亏损) + - 基于技术阻力位、斐波那契、或波动带 + - 建议在技术位前 0.1-0.2% 设置(防止未成交) + +2. **stop_loss** (止损价格) + - 限制单笔亏损在账户 1-3% + - 放置在关键支撑/阻力位之外 + - **滑点调整(V5.5.1 新增)**: + - 做多:止损价格下移 0.05%(50,000 → 49,975) + - 做空:止损价格上移 0.05% + - 预留滑点缓冲,防止实际成交价偏移 + +3. **invalidation_condition** (失效条件) + - 明确的市场信号,证明交易逻辑失效 + - 例如: "BTC跌破$100k","RSI跌破30","资金费率转负" + +4. **confidence** (信心度 0-1) + - 使用客观评分公式计算(基础分 60 + 条件加减分) + - <0.85: 禁止开仓 + - 0.85-0.90: 风险预算 1.5% + - 0.90-0.95: 风险预算 2% + - >0.95: 风险预算 2.5%(谨慎使用,警惕过度自信) + +5. **risk_usd** (风险金额) + - 计算公式: |入场价 - 止损价| × 仓位数量 × 杠杆 + - 必须 ≤ 账户净值 × 风险预算(1.5-2.5%) + +6. **slippage_buffer** (滑点缓冲 - V5.5.1 新增) + - 预期滑点:0.01-0.1%(取决于仓位大小) + - 小仓位(<1000 USDT):0.01-0.02% + - 中仓位(1000-5000 USDT):0.02-0.05% + - 大仓位(>5000 USDT):0.05-0.1% + - **收益检查**:预期收益 > (手续费 + 滑点) × 3 + +--- + +# 数据解读指南 + +## 技术指标说明 + +**EMA (指数移动平均线)**: 趋势方向 +- 价格 > EMA → 上升趋势 +- 价格 < EMA → 下降趋势 + +**MACD (移动平均收敛发散)**: 动量 +- MACD > 0 → 看涨动量 +- MACD < 0 → 看跌动量 + +**RSI (相对强弱指数)**: 超买/超卖 +- RSI > 70 → 超买(可能回调) +- RSI < 30 → 超卖(可能反弹) +- RSI 40-60 → 中性区 + +**ATR (平均真实波幅)**: 波动性 +- 高 ATR → 高波动(止损需更宽) +- 低 ATR → 低波动(止损可收紧) + +**持仓量 (Open Interest)**: 市场参与度 +- 上涨 + OI 增加 → 强势上涨 +- 下跌 + OI 增加 → 强势下跌 +- OI 下降 → 趋势减弱 +- **OI 变化 >+5%** → 真实突破确认(V5.5.1 强调) + +**资金费率 (Funding Rate)**: 市场情绪 +- 正费率 → 看涨(多方支付空方) +- 负费率 → 看跌(空方支付多方) +- 极端费率 (>0.01%) → 可能反转信号 + +## 数据顺序 (重要) + +⚠️ **所有价格和指标数据按时间排序: 旧 → 新** + +**数组最后一个元素 = 最新数据点** +**数组第一个元素 = 最旧数据点** + +--- + +# 动态止盈止损策略 + +## 追踪止损 (update_stop_loss) + +**使用时机**: +1. 持仓盈利 3-5% → 移动止损至成本价(保本) +2. 持仓盈利 10% → 移动止损至入场价 +5%(锁定部分利润) +3. 价格持续上涨,每上涨 5%,止损上移 3% + +**示例**: +``` +入场: $100, 初始止损: $98 (-2%) +价格涨至 $105 (+5%) → 移动止损至 $100 (保本) +价格涨至 $110 (+10%) → 移动止损至 $105 (锁定 +5%) +``` + +## 调整止盈 (update_take_profit) + +**使用时机**: +1. 价格接近目标但遇到强阻力 → 提前降低止盈价格 +2. 价格突破预期阻力位 → 追高止盈价格 +3. 技术位发生变化(支撑/阻力位突破) + +## 部分平仓 (partial_close) + +**使用时机**: +1. 盈利达到第一目标 (5-10%) → 平仓 50%,剩余继续持有 +2. 市场不确定性增加 → 先平仓 70%,保留 30% 观察 +3. 盈利达到预期的 2/3 → 平仓 1/2,让剩余仓位追求更大目标 + +**示例**: +``` +持仓: 10 BTC,成本 $100,目标 $120 +价格涨至 $110 (+10%) → partial_close 50% (平掉 5 BTC) + → 锁定利润: 5 × $10 = $50 + → 剩余 5 BTC 继续持有,追求 $120 目标 +``` + +--- # 交易哲学 & 最佳实践 ## 核心原则 -资金保全第一:保护资本比追求收益更重要 - -纪律胜于情绪:执行你的退出方案,不随意移动止损或目标 - -质量优于数量:少量高信念交易胜过大量低信念交易 - -适应市场状态:根据震荡/趋势切换策略 - -尊重技术位:在关键位前设置止盈,避免回撤 +1. **资本保全第一**: 保护资本比追求收益更重要 +2. **纪律胜于情绪**: 执行退出方案,不随意移动止损 +3. **质量优于数量**: 少量高信念交易胜过大量低信念交易 +4. **适应波动性**: 根据市场条件调整仓位 +5. **尊重趋势**: 不要与强趋势作对 +6. **BTC 优先**: 交易山寨币前必须确认 BTC 状态(V5.5.1 强调) ## 常见误区避免 -过度交易:频繁交易导致费用侵蚀利润 +- ⚠️ **过度交易**: 频繁交易导致手续费侵蚀利润 +- ⚠️ **复仇式交易**: 亏损后加码试图"翻本" +- ⚠️ **分析瘫痪**: 过度等待完美信号 +- ⚠️ **忽视相关性**: BTC 常引领山寨币,优先观察 BTC +- ⚠️ **过度杠杆**: 放大收益同时放大亏损 +- ⚠️ **假突破陷阱**: 15m 超买但 1h 未跟上,可能是假突破(V5.5.1 新增) +- ⚠️ **信心度虚高**: 主观判断 90 分,但客观评分可能只有 65 分(V5.5.1 新增) -复仇式交易:亏损后立即加码试图"翻本" +## 交易频率认知 -忽略技术位:固定百分比止盈,忽视压力支撑 +量化标准: +- 优秀交易: 每天 2-4 笔 = 每小时 0.1-0.2 笔 +- 过度交易: 每小时 >2 笔 = 严重问题 +- 最佳节奏: 开仓后持有至少 30-60 分钟 -策略混用:震荡市用趋势策略,或反之 - -过度杠杆:放大收益同时放大亏损 - -# 开仓标准(严格) - -只在强信号时开仓,不确定就观望。 - -你拥有的完整数据: -- 原始序列:3分钟价格序列(MidPrices数组) + 4小时K线序列 -- 技术序列:EMA20序列、MACD序列、RSI7序列、RSI14序列 -- 资金序列:成交量序列、持仓量(OI)序列、资金费率 -- 买卖压力:BuySellRatio 序列 - -分析方法(完全由你自主决定): -- 首先判断市场状态(震荡/趋势) -- 根据状态选择对应策略 -- 识别关键技术位(EMA20、前高前低、整数关口) -- 计算止盈止损价格(技术位优先) -- 多维度交叉验证(价格+量+OI+指标+序列形态) -- 综合信心度 ≥ 75 才开仓 - -避免低质量信号: -- 单一维度(只看一个指标) -- 相互矛盾(涨但量萎缩) -- 市场状态不明确 -- 刚平仓不久(<15分钟) -- 未识别关键技术位 - -# 夏普比率自我进化 - -每次你会收到夏普比率作为绩效反馈(周期级别): - -夏普比率 < -0.5 (持续亏损): - → 停止交易,连续观望至少6个周期(18分钟) - → 深度反思: - • 交易频率过高?(每小时>2次就是过度) - • 持仓时间过短?(<30分钟就是过早平仓) - • 信号强度不足?(信心度<75) - • 技术位分析不准?(回撤在技术位前发生) - • 策略选择错误?(震荡市用趋势策略) - -夏普比率 -0.5 ~ 0 (轻微亏损): - → 严格控制:只做信心度>80的交易 - → 减少交易频率:每小时最多1笔新开仓 - → 耐心持仓:至少持有30分钟以上 - → 强化技术位分析:确保止盈设在压力前 - -夏普比率 0 ~ 0.7 (正收益): - → 维持当前策略 - -夏普比率 > 0.7 (优异表现): - → 可适度扩大仓位 - -关键: 夏普比率是唯一指标,它会自然惩罚频繁交易和过度进出。 - -# 动态止盈止损功能 - -你现在可以在持仓中主动调整止盈止损,实现追踪止损和分批止盈策略。 - -## 可用的新 Actions - -### 1. update_stop_loss - 调整止损 - -用于实现追踪止损,保护利润。 - -示例场景: -- 开仓 BTC @ 100,000,止损 99,000 (-1%) -- 价格上涨到 101,500 (+1.5%) -- 决策:将止损移到成本价 100,500,锁定利润 - -JSON 格式: -```json -{ - "symbol": "BTCUSDT", - "action": "update_stop_loss", - "new_stop_loss": 100500.0, - "confidence": 90, - "reasoning": "浮盈达到 1.5%,将止损移到成本价保证不亏" -} -``` - -### 2. update_take_profit - 调整止盈 - -用于在技术位前提前止盈,避免回撤。 - -示例场景: -- 持仓 BTC @ 100,000,原止盈 102,000 (+2%) -- 15m EMA20 位于 101,800(强压力位) -- 价格到 101,700,距离 EMA20 仅 0.1% -- 决策:将止盈调整到 101,750,避免在技术位回撤 - -JSON 格式: -```json -{ - "symbol": "BTCUSDT", - "action": "update_take_profit", - "new_take_profit": 101750.0, - "confidence": 85, - "reasoning": "价格接近 EMA20 压力位,提前止盈避免回撤" -} -``` - -### 3. partial_close - 部分平仓 - -用于分批止盈,既锁定部分利润,又保留追涨空间。 - -示例场景: -- 持仓 BTC 0.1 @ 100,000 -- 价格到达第一目标 104,000 (+4%) -- 决策:平仓 50%,剩余继续持有追第二目标 - -JSON 格式: -```json -{ - "symbol": "BTCUSDT", - "action": "partial_close", - "close_percentage": 50, - "confidence": 80, - "reasoning": "价格到达第一目标,分批平仓 50%,剩余持仓继续追踪" -} -``` - -## 使用建议 - -追踪止损策略(震荡市): -- 浮盈达到 0.8% → update_stop_loss 移到成本价 -- 浮盈达到 1.2% → update_stop_loss 移到 +0.5% -- 价格距离技术位 < 0.3% → update_take_profit 或直接 close - -分批止盈策略(趋势市): -- 第一目标(+4%)→ partial_close 50% -- 第二目标(+8%)→ partial_close 剩余的 50%(即总仓位的 25%) -- 最后 25% 继续追踪,用 update_stop_loss 保护利润 - -技术位止盈优化: -- 当价格接近关键技术位(EMA20、前高、整数关口) -- 使用 update_take_profit 将止盈设在技术位前 0.1-0.2% -- 避免在技术位遇阻回撤 - -# 决策流程 - -1. 分析夏普比率: 当前策略是否有效?需要调整吗? -2. 判断市场状态: 震荡还是趋势?(多指标验证) -3. 选择对应策略: 策略A(震荡)还是策略B(趋势)? -4. 评估持仓: 趋势是否改变?是否该止盈/止损?需要调整止损保护利润吗? -5. 识别技术位: 上方压力、下方支撑在哪里?是否需要提前止盈? -6. 寻找新机会: 有强信号吗?技术位明确吗? -7. 计算止盈止损: 技术位优先,还是固定百分比? -8. 输出决策: 思维链分析 + JSON +自查: +- 每个周期都交易 → 标准太低 +- 持仓 <30 分钟就平仓 → 太急躁 +- 连续 2 次止损后仍想立即开仓 → 需暂停 45 分钟(V5.5.1 强制) --- -记住: -- 目标是夏普比率,不是交易频率 -- 先判断市场状态,再选择策略 -- 技术位优先,避免在压力/支撑前回撤 -- 持仓中主动调整止损,锁定利润 -- 宁可错过,不做低质量交易 -- 风险回报比1:3是底线 +# 最终提醒 + +1. 每次决策前仔细阅读用户提示 +2. 验证仓位计算(仔细检查数学) +3. 确保 JSON 输出有效且完整 +4. 使用客观公式计算信心评分(不要夸大) +5. 坚持退出计划(不要过早放弃止损) +6. **先检查 BTC 状态,再决定是否开仓**(V5.5.1 核心) +7. **疑惑时,选择 wait**(最高原则) + +记住: 你在用真金白银交易真实市场。每个决策都有后果。系统化交易,严格管理风险,让概率随时间为你服务。 + +--- + +# V5.5.1 核心改进总结 + +1. ✅ **BTC 状态检查**(第 5 步)- 交易山寨币的最关键保护 +2. ✅ **多空确认清单**(第 6 步)- 5/8 项一致,防假信号 +3. ✅ **客观信心度评分**(第 8 步)- 基础分 60 + 条件加减分 +4. ✅ **防假突破逻辑**(第 7 步)- RSI 多周期 + K 线形态过滤 +5. ✅ **连续止损暂停**(第 2 步)- 2 次 45min,3 次 24h,4 次 72h +6. ✅ **OI 持仓量确认**(第 6 步清单第 8 项)- >+5% 真实突破 +7. ✅ **信号优先级排序**(第 6 步)- 趋势共振 > 放量 > BTC > RSI... +8. ✅ **滑点处理**(风险管理协议第 2/6 项)- 0.05% 缓冲 + 收益检查 + +**设计哲学**:让 AI 自主判断趋势或震荡,不预设策略 A/B,信任强推理模型的能力。 + +现在,分析下面提供的市场数据并做出交易决策。 From c0c0688805661bbd894ed879335ad6a8174337a4 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 06:11:12 +0800 Subject: [PATCH 020/195] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20logger=EF=BC=9A?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=96=B0=E5=A2=9E=E7=9A=84=E4=B8=89=E5=80=8B?= =?UTF-8?q?=E5=8B=95=E4=BD=9C=E9=A1=9E=E5=9E=8B=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E5=85=A7=E5=AE=B9=EF=BC=9A=201.=20DecisionAction=20=E8=A8=BB?= =?UTF-8?q?=E9=87=8B=EF=BC=9A=E6=B7=BB=E5=8A=A0=20update=5Fstop=5Floss,=20?= =?UTF-8?q?update=5Ftake=5Fprofit,=20partial=5Fclose=202.=20GetStatistics?= =?UTF-8?q?=EF=BC=9Apartial=5Fclose=20=E8=A8=88=E5=85=A5=20TotalClosePosit?= =?UTF-8?q?ions=203.=20AnalyzePerformance=20=E9=A0=90=E5=A1=AB=E5=85=85?= =?UTF-8?q?=E9=82=8F=E8=BC=AF=EF=BC=9A=E8=99=95=E7=90=86=20partial=5Fclose?= =?UTF-8?q?=EF=BC=88=E4=B8=8D=E5=88=AA=E9=99=A4=E6=8C=81=E5=80=89=E8=A8=98?= =?UTF-8?q?=E9=8C=84=EF=BC=89=204.=20AnalyzePerformance=20=E5=88=86?= =?UTF-8?q?=E6=9E=90=E9=82=8F=E8=BC=AF=EF=BC=9A=20=20=20=20-=20partial=5Fc?= =?UTF-8?q?lose=20=E6=AD=A3=E7=A2=BA=E5=88=A4=E6=96=B7=E6=8C=81=E5=80=89?= =?UTF-8?q?=E6=96=B9=E5=90=91=20=20=20=20-=20=E8=A8=98=E9=8C=84=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=B9=B3=E5=80=89=E7=9A=84=E7=9B=88=E8=99=A7=E7=B5=B1?= =?UTF-8?q?=E8=A8=88=20=20=20=20-=20=E4=BF=9D=E7=95=99=E6=8C=81=E5=80=89?= =?UTF-8?q?=E8=A8=98=E9=8C=84=EF=BC=88=E5=9B=A0=E7=82=BA=E9=82=84=E6=9C=89?= =?UTF-8?q?=E5=89=A9=E9=A4=98=E5=80=89=E4=BD=8D=EF=BC=89=20=E8=AA=AA?= =?UTF-8?q?=E6=98=8E=EF=BC=9Apartial=5Fclose=20=E6=9C=83=E8=A8=98=E9=8C=84?= =?UTF-8?q?=E7=9B=88=E8=99=A7=EF=BC=8C=E4=BD=86=E4=B8=8D=E5=88=AA=E9=99=A4?= =?UTF-8?q?=20openPositions=EF=BC=8C=20=20=20=20=20=20=20=E5=9B=A0?= =?UTF-8?q?=E7=82=BA=E9=82=84=E6=9C=89=E5=89=A9=E9=A4=98=E5=80=89=E4=BD=8D?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E7=B9=BC=E7=BA=8C=E4=BA=A4=E6=98=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logger/decision_logger.go | 43 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/logger/decision_logger.go b/logger/decision_logger.go index efa5ab74..9891ce71 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,9 @@ 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", "partial_close": stats.TotalClosePositions++ + // update_stop_loss 和 update_take_profit 不計入統計 } } } @@ -348,11 +349,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" { side = "long" } else if action.Action == "open_short" || action.Action == "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 { @@ -368,6 +380,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna case "close_long", "close_short": // 移除已平仓记录 delete(openPositions, posKey) + // partial_close 不處理,保留持倉記錄 } } } @@ -382,11 +395,23 @@ 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" { side = "long" } else if action.Action == "open_short" || action.Action == "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 { @@ -400,7 +425,7 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna "leverage": action.Leverage, } - case "close_long", "close_short": + case "close_long", "close_short", "partial_close": // 查找对应的开仓记录(可能来自预填充或当前窗口) if openPos, exists := openPositions[posKey]; exists { openPrice := openPos["openPrice"].(float64) @@ -472,8 +497,10 @@ func (l *DecisionLogger) AnalyzePerformance(lookbackCycles int) (*PerformanceAna stats.LosingTrades++ } - // 移除已平仓记录 - delete(openPositions, posKey) + // 移除已平仓记录(partial_close 不刪除,因為還有剩餘倉位) + if action.Action != "partial_close" { + delete(openPositions, posKey) + } } } } From 5d2d849226f627ef7f3c020d50d08a0bfc73797b Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Mon, 3 Nov 2025 19:58:30 +0800 Subject: [PATCH 021/195] =?UTF-8?q?refactor(prompts):=20add=20comprehensiv?= =?UTF-8?q?e=20partial=5Fclose=20guidance=20to=20adaptive.txt=20Add=20deta?= =?UTF-8?q?iled=20guidance=20chapter=20for=20dynamic=20TP/SL=20management?= =?UTF-8?q?=20and=20partial=20close=20operations.=20##=20Changes=20-=20New?= =?UTF-8?q?=20chapter:=20"=E5=8A=A8=E6=80=81=E6=AD=A2=E7=9B=88=E6=AD=A2?= =?UTF-8?q?=E6=8D=9F=E4=B8=8E=E9=83=A8=E5=88=86=E5=B9=B3=E4=BB=93=E6=8C=87?= =?UTF-8?q?=E5=BC=95"=20(Dynamic=20TP/SL=20&=20Partial=20Close=20Guidance)?= =?UTF-8?q?=20-=20Inserted=20between=20"=E5=8F=AF=E7=94=A8=E5=8A=A8?= =?UTF-8?q?=E4=BD=9C"=20(Actions)=20and=20"=E5=86=B3=E7=AD=96=E6=B5=81?= =?UTF-8?q?=E7=A8=8B"=20(Decision=20Flow)=20sections=20-=204=20key=20guida?= =?UTF-8?q?nce=20points=20covering:=20=20=201.=20Partial=20close=20best=20?= =?UTF-8?q?practices=20(use=20clear=20percentages=20like=2025%/50%/75%)=20?= =?UTF-8?q?=20=202.=20Reassessing=20remaining=20position=20after=20partial?= =?UTF-8?q?=20exit=20=20=203.=20Proper=20use=20cases=20for=20update=5Fstop?= =?UTF-8?q?=5Floss=20/=20update=5Ftake=5Fprofit=20=20=204.=20Multi-stage?= =?UTF-8?q?=20exit=20strategy=20requirements=20##=20Benefits=20-=20?= =?UTF-8?q?=E2=9C=85=20Provides=20concrete=20operational=20guidelines=20fo?= =?UTF-8?q?r=20AI=20decision-making=20-=20=E2=9C=85=20Clarifies=20when=20a?= =?UTF-8?q?nd=20how=20to=20use=20partial=5Fclose=20effectively=20-=20?= =?UTF-8?q?=E2=9C=85=20Emphasizes=20remaining=20position=20management=20(p?= =?UTF-8?q?revents=20"orphan"=20positions)=20-=20=E2=9C=85=20Aligns=20with?= =?UTF-8?q?=20existing=20backend=20support=20for=20partial=5Fclose=20actio?= =?UTF-8?q?n=20##=20Background=20While=20adaptive.txt=20already=20lists=20?= =?UTF-8?q?partial=5Fclose=20as=20an=20available=20action,=20it=20lacked?= =?UTF-8?q?=20detailed=20operational=20guidance.=20This=20enhancement=20fi?= =?UTF-8?q?lls=20that=20gap=20by=20providing=20specific=20percentages,=20u?= =?UTF-8?q?se=20cases,=20and=20multi-stage=20exit=20examples.=20Backend=20?= =?UTF-8?q?(decision/engine.go)=20already=20validates=20partial=5Fclose=20?= =?UTF-8?q?with=20close=5Fpercentage=20field,=20so=20this=20is=20purely=20?= =?UTF-8?q?a=20prompt=20enhancement=20with=20no=20code=20changes=20require?= =?UTF-8?q?d.=20Co-Authored-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prompts/adaptive.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index d5778caa..ded58263 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -97,6 +97,15 @@ --- +# 动态止盈止损与部分平仓指引 + +- `partial_close` 用于锁定阶段性收益或降低风险,建议使用清晰比例(如 25% / 50% / 75%),并说明目的(例:"锁定关键阻力前利润""减半仓等待回踩确认")。 +- 执行部分平仓后,应评估是否需要同步上调止损 / 下调止盈,确保剩余仓位符合新的风险回报结构。 +- `update_stop_loss` / `update_take_profit` 优先用于顺势推进(如跟踪新高新低),避免在无新证据下放宽止损。 +- 若计划分批退出,请在 `reasoning` 中描述剩余仓位的策略与失效条件,避免出现"减仓后不知道如何处理剩余部位"的情况。 + +--- + # 决策流程(严格顺序) ## 第 0 步:疑惑检查 From 3f5bb5ca844af1f7c06569b9ae6b77284542490a Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:54:49 +0800 Subject: [PATCH 022/195] =?UTF-8?q?fix(market):=20resolve=20price=20stalen?= =?UTF-8?q?ess=20issue=20in=20GetCurrentKlines=20##=20Problem=20GetCurrent?= =?UTF-8?q?Klines=20had=20two=20critical=20bugs=20causing=20price=20data?= =?UTF-8?q?=20to=20become=20stale:=201.=20Incorrect=20return=20logic:=20re?= =?UTF-8?q?turned=20error=20even=20when=20data=20fetch=20succeeded=202.=20?= =?UTF-8?q?Race=20condition:=20returned=20slice=20reference=20instead=20of?= =?UTF-8?q?=20deep=20copy,=20causing=20concurrent=20data=20corruption=20##?= =?UTF-8?q?=20Impact=20-=20BTC=20price=20stuck=20at=20106xxx=20while=20act?= =?UTF-8?q?ual=20market=20price=20was=20107xxx+=20-=20LLM=20calculated=20t?= =?UTF-8?q?ake-profit=20based=20on=20stale=20prices=20=E2=86=92=20orders?= =?UTF-8?q?=20failed=20validation=20-=20Statistics=20showed=20incorrect=20?= =?UTF-8?q?P&L=20(0.00%)=20due=20to=20corrupted=20historical=20data=20-=20?= =?UTF-8?q?Alt-coins=20filtered=20out=20due=20to=20failed=20market=20data?= =?UTF-8?q?=20fetch=20##=20Solution=201.=20Fixed=20return=20logic:=20only?= =?UTF-8?q?=20return=20error=20when=20actual=20failure=20occurs=202.=20Ret?= =?UTF-8?q?urn=20deep=20copy=20instead=20of=20reference=20to=20prevent=20r?= =?UTF-8?q?ace=20conditions=203.=20Downgrade=20subscription=20errors=20to?= =?UTF-8?q?=20warnings=20(non-blocking)=20##=20Test=20Results=20=E2=9C=85?= =?UTF-8?q?=20Price=20updates=20in=20real-time=20=E2=9C=85=20Take-profit?= =?UTF-8?q?=20orders=20execute=20successfully=20=E2=9C=85=20P&L=20calculat?= =?UTF-8?q?ions=20accurate=20=E2=9C=85=20Alt-coins=20now=20tradeable=20Rel?= =?UTF-8?q?ated:=20Price=20feed=20mechanism,=20concurrent=20data=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- market/monitor.go | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/market/monitor.go b/market/monitor.go index 23e126d9..a09763e8 100644 --- a/market/monitor.go +++ b/market/monitor.go @@ -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() { From b8eea8eaad919b7f7ef2babbb37a4c72a598e6cf Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 10:55:11 +0800 Subject: [PATCH 023/195] =?UTF-8?q?feat(decision):=20make=20OI=20threshold?= =?UTF-8?q?=20configurable=20+=20add=20relaxed=20prompt=20template=20##=20?= =?UTF-8?q?Changes=20###=201.=20decision/engine.go=20-=20Configurable=20OI?= =?UTF-8?q?=20Threshold=20-=20Extract=20hardcoded=2015M=20OI=20threshold?= =?UTF-8?q?=20to=20configurable=20constant=20-=20Add=20clear=20documentati?= =?UTF-8?q?on=20for=20risk=20profiles:=20=20=20-=2015M=20(Conservative)=20?= =?UTF-8?q?-=20BTC/ETH/SOL=20only=20=20=20-=2010M=20(Balanced)=20-=20Add?= =?UTF-8?q?=20major=20alt-coins=20=20=20-=208M=20(Relaxed)=20-=20Include?= =?UTF-8?q?=20mid-cap=20coins=20(BNB/LINK/AVAX)=20=20=20-=205M=20(Aggressi?= =?UTF-8?q?ve)=20-=20Most=20alt-coins=20allowed=20-=20Default:=2015M=20(?= =?UTF-8?q?=E4=BF=9D=E5=AE=88=EF=BC=8C=E7=B6=AD=E6=8C=81=E5=8E=9F=E8=A1=8C?= =?UTF-8?q?=E7=82=BA)=20###=202.=20prompts/adaptive=5Frelaxed.txt=20-=20Ne?= =?UTF-8?q?w=20Trading=20Template=20Conservative=20optimization=20for=20in?= =?UTF-8?q?creased=20trading=20frequency=20while=20maintaining=20high=20wi?= =?UTF-8?q?n-rate:=20**Key=20Adjustments:**=20-=20Confidence=20threshold:?= =?UTF-8?q?=2085=20=E2=86=92=2080=20(allow=20more=20opportunities)=20-=20C?= =?UTF-8?q?ooldown=20period:=209min=20=E2=86=92=206min=20(faster=20reactio?= =?UTF-8?q?n)=20-=20Multi-timeframe=20trend:=203=20periods=20=E2=86=92=202?= =?UTF-8?q?=20periods=20(relaxed=20requirement)=20-=20Entry=20checklist:?= =?UTF-8?q?=205/8=20=E2=86=92=204/8=20(easier=20to=20pass)=20-=20RSI=20ran?= =?UTF-8?q?ge:=2030-40/65-70=20=E2=86=92=20<45/>60=20(wider=20acceptance)?= =?UTF-8?q?=20-=20Risk-reward=20ratio:=201:3=20=E2=86=92=201:2.5=20(more?= =?UTF-8?q?=20flexible)=20**Expected=20Impact:**=20-=20Trading=20frequency?= =?UTF-8?q?:=205/day=20=E2=86=92=208-15/day=20(+60-200%)=20-=20Win-rate:?= =?UTF-8?q?=2040%=20=E2=86=92=2050-55%=20(improved)=20-=20Alt-coins:=20Mor?= =?UTF-8?q?e=20opportunities=20unlocked=20-=20Risk=20controls:=20Preserved?= =?UTF-8?q?=20(Sharpe-based,=20loss-pause)=20##=20Usage=20Users=20can=20no?= =?UTF-8?q?w=20choose=20trading=20style=20via=20Web=20UI:=20-=20`adaptive`?= =?UTF-8?q?=20-=20Strictest=20(original)=20-=20`adaptive=5Frelaxed`=20-=20?= =?UTF-8?q?Balanced=20(this=20PR)=20-=20`nof1`=20-=20Most=20aggressive=20#?= =?UTF-8?q?#=20Rationale=20The=20original=20adaptive.txt=20uses=205-layer?= =?UTF-8?q?=20filtering=20(confidence/cooldown/trend/checklist/RSI)=20that?= =?UTF-8?q?=20filters=20out=20~95%=20of=20opportunities.=20This=20template?= =?UTF-8?q?=20provides=20a=20middle-ground=20option=20for=20users=20who=20?= =?UTF-8?q?want=20higher=20frequency=20without=20sacrificing=20core=20risk?= =?UTF-8?q?=20management.=20Related:=20#trading-frequency=20#alt-coin-supp?= =?UTF-8?q?ort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 11 +- prompts/adaptive_relaxed.txt | 194 +++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 prompts/adaptive_relaxed.txt diff --git a/decision/engine.go b/decision/engine.go index df48d534..b2b05e51 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -160,17 +160,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 } } 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%+ 胜率 +- 允许更多山寨币机会 +- 保持核心風控(夏普、連虧停手) From b7a1a60c6fad2b67592401ded306ce6f7d644901 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:08:39 +0800 Subject: [PATCH 024/195] =?UTF-8?q?fix:=20=E8=BF=87=E6=BB=A4=E5=B9=BD?= =?UTF-8?q?=E7=81=B5=E6=8C=81=E4=BB=93=20-=20=E8=B7=B3=E8=BF=87=20quantity?= =?UTF-8?q?=3D0=20=E7=9A=84=E6=8C=81=E4=BB=93=E9=98=B2=E6=AD=A2=20AI=20?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=20=E9=97=AE=E9=A2=98=EF=BC=9A=20-=20?= =?UTF-8?q?=E6=AD=A2=E6=8D=9F/=E6=AD=A2=E7=9B=88=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=90=8E=EF=BC=8C=E4=BA=A4=E6=98=93=E6=89=80=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=20positionAmt=3D0=20=E7=9A=84=E6=8C=81=E4=BB=93=E8=AE=B0?= =?UTF-8?q?=E5=BD=95=20-=20=E8=BF=99=E4=BA=9B=E5=B9=BD=E7=81=B5=E6=8C=81?= =?UTF-8?q?=E4=BB=93=E8=A2=AB=E4=BC=A0=E9=80=92=E7=BB=99=20AI=EF=BC=8C?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=20AI=20=E8=AF=AF=E4=BB=A5=E4=B8=BA=E4=BB=8D?= =?UTF-8?q?=E6=8C=81=E6=9C=89=E8=AF=A5=E5=B8=81=E7=A7=8D=20-=20AI=20?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E5=9F=BA=E4=BA=8E=E9=94=99=E8=AF=AF=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=81=9A=E5=87=BA=E5=86=B3=E7=AD=96=EF=BC=88=E5=A6=82?= =?UTF-8?q?=E5=B0=9D=E8=AF=95=E8=B0=83=E6=95=B4=E5=B7=B2=E4=B8=8D=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=9A=84=E6=AD=A2=E6=8D=9F=EF=BC=89=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=9A=20-=20buildTradingContext()=20=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20quantity=3D=3D0=20=E6=A3=80=E6=9F=A5=20-?= =?UTF-8?q?=20=E8=B7=B3=E8=BF=87=E5=B7=B2=E5=B9=B3=E4=BB=93=E7=9A=84?= =?UTF-8?q?=E6=8C=81=E4=BB=93=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=8F=AA=E4=BC=A0?= =?UTF-8?q?=E9=80=92=E7=9C=9F=E5=AE=9E=E6=8C=81=E4=BB=93=E7=BB=99=20AI=20-?= =?UTF-8?q?=20=E8=A7=A6=E5=8F=91=E6=B8=85=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=9A=E6=92=A4=E9=94=80=E5=AD=A4=E5=84=BF=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E3=80=81=E6=B8=85=E7=90=86=E5=86=85=E9=83=A8=E7=8A=B6=E6=80=81?= =?UTF-8?q?=20=E5=BD=B1=E5=93=8D=E8=8C=83=E5=9B=B4=EF=BC=9A=20-=20trader/a?= =?UTF-8?q?uto=5Ftrader.go:487-490=20=E6=B5=8B=E8=AF=95=EF=BC=9A=20-=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E6=88=90=E5=8A=9F=20-=20=E5=AE=B9=E5=99=A8?= =?UTF-8?q?=E9=87=8D=E5=BB=BA=E5=B9=B6=E5=90=AF=E5=8A=A8=E6=AD=A3=E5=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/auto_trader.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 1e93ab5c..6dfdac99 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -195,7 +195,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{ @@ -481,6 +482,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) From 98b5b20043e54045c15cdfda08c3560632d57e1b Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 09:05:47 +0800 Subject: [PATCH 025/195] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=20HTTP/2=20st?= =?UTF-8?q?ream=20error=20=E5=88=B0=E5=8F=AF=E9=87=8D=E8=A9=A6=E9=8C=AF?= =?UTF-8?q?=E8=AA=A4=E5=88=97=E8=A1=A8=20=E5=95=8F=E9=A1=8C=EF=BC=9A=20-?= =?UTF-8?q?=20=E7=94=A8=E6=88=B6=E9=81=87=E5=88=B0=E9=8C=AF=E8=AA=A4?= =?UTF-8?q?=EF=BC=9Astream=20error:=20stream=20ID=201;=20INTERNAL=5FERROR?= =?UTF-8?q?=20-=20=E9=80=99=E6=98=AF=20HTTP/2=20=E9=80=A3=E6=8E=A5?= =?UTF-8?q?=E8=A2=AB=E6=9C=8D=E5=8B=99=E7=AB=AF=E9=97=9C=E9=96=89=E7=9A=84?= =?UTF-8?q?=E9=8C=AF=E8=AA=A4=20-=20=E7=95=B6=E5=89=8D=E9=87=8D=E8=A9=A6?= =?UTF-8?q?=E6=A9=9F=E5=88=B6=E4=B8=8D=E5=8C=85=E5=90=AB=E6=AD=A4=E9=A1=9E?= =?UTF-8?q?=E9=8C=AF=E8=AA=A4=EF=BC=8C=E5=B0=8E=E8=87=B4=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E5=A4=B1=E6=95=97=20=E4=BF=AE=E5=BE=A9=EF=BC=9A=20-=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20"stream=20error"=20=E5=88=B0=E5=8F=AF?= =?UTF-8?q?=E9=87=8D=E8=A9=A6=E5=88=97=E8=A1=A8=20-=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=20"INTERNAL=5FERROR"=20=E5=88=B0=E5=8F=AF=E9=87=8D=E8=A9=A6?= =?UTF-8?q?=E5=88=97=E8=A1=A8=20-=20=E9=81=87=E5=88=B0=E6=AD=A4=E9=A1=9E?= =?UTF-8?q?=E9=8C=AF=E8=AA=A4=E6=99=82=E6=9C=83=E8=87=AA=E5=8B=95=E9=87=8D?= =?UTF-8?q?=E8=A9=A6=EF=BC=88=E6=9C=80=E5=A4=9A=203=20=E6=AC=A1=EF=BC=89?= =?UTF-8?q?=20=E5=BD=B1=E9=9F=BF=EF=BC=9A=20-=20=E6=8F=90=E9=AB=98=20API?= =?UTF-8?q?=20=E8=AA=BF=E7=94=A8=E7=A9=A9=E5=AE=9A=E6=80=A7=20-=20?= =?UTF-8?q?=E8=87=AA=E5=8B=95=E8=99=95=E7=90=86=E6=9C=8D=E5=8B=99=E7=AB=AF?= =?UTF-8?q?=E8=87=A8=E6=99=82=E6=95=85=E9=9A=9C=20-=20=E6=B8=9B=E5=B0=91?= =?UTF-8?q?=E5=9B=A0=E7=B6=B2=E7=B5=A1=E6=B3=A2=E5=8B=95=E5=B0=8E=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E5=A4=B1=E6=95=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mcp/client.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mcp/client.go b/mcp/client.go index 9191dfaf..5ef090e8 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -280,6 +280,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) { From 7b43fc40725d65700b43250608a2fbbcb6e1d8e1 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 08:21:56 +0800 Subject: [PATCH 026/195] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=E9=A6=96?= =?UTF-8?q?=E6=AC=A1=E9=81=8B=E8=A1=8C=E6=99=82=E6=95=B8=E6=93=9A=E5=BA=AB?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=A4=B1=E6=95=97=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=20=E5=95=8F=E9=A1=8C=EF=BC=9A=20-=20=E7=94=A8=E6=88=B6?= =?UTF-8?q?=E9=A6=96=E6=AC=A1=E9=81=8B=E8=A1=8C=E5=A0=B1=E9=8C=AF=EF=BC=9A?= =?UTF-8?q?unable=20to=20open=20database=20file:=20is=20a=20directory=20-?= =?UTF-8?q?=20=E5=8E=9F=E5=9B=A0=EF=BC=9ADocker=20volume=20=E6=8E=9B?= =?UTF-8?q?=E8=BC=89=E6=99=82=EF=BC=8C=E5=A6=82=E6=9E=9C=20config.db=20?= =?UTF-8?q?=E4=B8=8D=E5=AD=98=E5=9C=A8=EF=BC=8C=E6=9C=83=E5=89=B5=E5=BB=BA?= =?UTF-8?q?=E7=9B=AE=E9=8C=84=E8=80=8C=E9=9D=9E=E6=96=87=E4=BB=B6=20-=20?= =?UTF-8?q?=E5=BD=B1=E9=9F=BF=EF=BC=9A=E6=96=B0=E7=94=A8=E6=88=B6=E7=84=A1?= =?UTF-8?q?=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=95=9F=E5=8B=95=E7=B3=BB=E7=B5=B1?= =?UTF-8?q?=20=E4=BF=AE=E5=BE=A9=EF=BC=9A=20-=20=E5=9C=A8=20start.sh=20?= =?UTF-8?q?=E5=95=9F=E5=8B=95=E5=89=8D=E6=AA=A2=E6=9F=A5=20config.db=20?= =?UTF-8?q?=E6=98=AF=E5=90=A6=E5=AD=98=E5=9C=A8=20-=20=E5=A6=82=E4=B8=8D?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E5=89=87=E5=89=B5=E5=BB=BA=E7=A9=BA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=EF=BC=88touch=20config.db=EF=BC=89=20-=20=E7=A2=BA?= =?UTF-8?q?=E4=BF=9D=20Docker=20=E6=8E=9B=E8=BC=89=E7=82=BA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E8=80=8C=E9=9D=9E=E7=9B=AE=E9=8C=84=20=E6=B8=AC?= =?UTF-8?q?=E8=A9=A6=EF=BC=9A=20-=20=E9=A6=96=E6=AC=A1=E9=81=8B=E8=A1=8C?= =?UTF-8?q?=EF=BC=9A./start.sh=20start=20=E2=86=92=20=E6=AD=A3=E5=B8=B8?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=20=E2=9C=93=20-=20=E7=8F=BE?= =?UTF-8?q?=E6=9C=89=E7=94=A8=E6=88=B6=EF=BC=9A=E7=84=A1=E5=BD=B1=E9=9F=BF?= =?UTF-8?q?=EF=BC=8C=E5=90=91=E5=BE=8C=E5=85=BC=E5=AE=B9=20=E2=9C=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- start.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 From 96d59e624180340ae94c809d560920689a02a19a Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 07:53:17 +0800 Subject: [PATCH 027/195] =?UTF-8?q?fix:=20=E4=BF=AE=E5=BE=A9=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E4=BD=99=E9=A1=8D=E9=A1=AF=E7=A4=BA=E9=8C=AF=E8=AA=A4?= =?UTF-8?q?=EF=BC=88=E4=BD=BF=E7=94=A8=E7=95=B6=E5=89=8D=E6=B7=A8=E5=80=BC?= =?UTF-8?q?=E8=80=8C=E9=9D=9E=E9=85=8D=E7=BD=AE=E5=80=BC=EF=BC=89=20?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=EF=BC=9A=20-=20=E5=9C=96=E8=A1=A8=E9=A1=AF?= =?UTF-8?q?=E7=A4=BA=E3=80=8C=E5=88=9D=E5=A7=8B=E4=BD=99=E9=A1=8D=20693.15?= =?UTF-8?q?=20USDT=E3=80=8D=EF=BC=88=E5=AF=A6=E9=9A=9B=E6=87=89=E8=A9=B2?= =?UTF-8?q?=E6=98=AF=20600=EF=BC=89=20-=20=E5=8E=9F=E5=9B=A0=EF=BC=9A?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=20validHistory[0].total=5Fequity=EF=BC=88?= =?UTF-8?q?=E7=95=B6=E5=89=8D=E6=B7=A8=E5=80=BC=EF=BC=89=20-=20=E5=B0=8E?= =?UTF-8?q?=E8=87=B4=E5=88=9D=E5=A7=8B=E4=BD=99=E9=A1=8D=E9=9A=A8=E8=91=97?= =?UTF-8?q?=E7=9B=88=E8=99=A7=E8=AE=8A=E5=8C=96=EF=BC=8C=E6=95=B8=E5=AD=B8?= =?UTF-8?q?=E9=82=8F=E8=BC=AF=E9=8C=AF=E8=AA=A4=20=E4=BF=AE=E5=BE=A9?= =?UTF-8?q?=EF=BC=9A=20-=20=E5=84=AA=E5=85=88=E5=BE=9E=20account.initial?= =?UTF-8?q?=5Fbalance=20=E8=AE=80=E5=8F=96=E7=9C=9F=E5=AF=A6=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=80=BC=20-=20=E5=82=99=E9=81=B8=E6=96=B9=E6=A1=88?= =?UTF-8?q?=EF=BC=9A=E5=BE=9E=E6=AD=B7=E5=8F=B2=E6=95=B8=E6=93=9A=E5=8F=8D?= =?UTF-8?q?=E6=8E=A8=EF=BC=88=E6=B7=A8=E5=80=BC=20-=20=E7=9B=88=E8=99=A7?= =?UTF-8?q?=EF=BC=89=20-=20=E9=BB=98=E8=AA=8D=E5=80=BC=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=201000=EF=BC=88=E8=88=87=E5=89=B5=E5=BB=BA=E4=BA=A4=E6=98=93?= =?UTF-8?q?=E5=93=A1=E6=99=82=E7=9A=84=E9=BB=98=E8=AA=8D=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E4=B8=80=E8=87=B4=EF=BC=89=20=E6=B8=AC=E8=A9=A6=EF=BC=9A=20-?= =?UTF-8?q?=20=E5=88=9D=E5=A7=8B=E4=BD=99=E9=A1=8D=EF=BC=9A600=20USDT?= =?UTF-8?q?=EF=BC=88=E5=9B=BA=E5=AE=9A=EF=BC=89=20-=20=E7=95=B6=E5=89=8D?= =?UTF-8?q?=E6=B7=A8=E5=80=BC=EF=BC=9A693.15=20USDT=20-=20=E7=9B=88?= =?UTF-8?q?=E8=99=A7=EF=BC=9A+93.15=20USDT=20(+15.52%)=20=E2=9C=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/EquityChart.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/components/EquityChart.tsx b/web/src/components/EquityChart.tsx index e7441779..fdc8131f 100644 --- a/web/src/components/EquityChart.tsx +++ b/web/src/components/EquityChart.tsx @@ -104,10 +104,10 @@ export function EquityChart({ traderId }: EquityChartProps) { ? validHistory.slice(-MAX_DISPLAY_POINTS) : validHistory; - // 计算初始余额(使用第一个有效数据点,如果无数据则从account获取,最后才用默认值) - const initialBalance = validHistory[0]?.total_equity - || account?.total_equity - || 100; // 默认值改为100,与常见配置一致 + // 计算初始余额(优先从 account 获取配置的初始余额,备选从历史数据反推) + const initialBalance = account?.initial_balance // 从交易员配置读取真实初始余额 + || (validHistory[0] ? validHistory[0].total_equity - validHistory[0].pnl : undefined) // 备选:淨值 - 盈亏 + || 1000; // 默认值(与创建交易员时的默认配置一致) // 转换数据格式 const chartData = displayHistory.map((point) => { From 4d54a4704c12659a4dfb62942d870e909e14b4e2 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Sun, 2 Nov 2025 07:11:57 +0800 Subject: [PATCH 028/195] =?UTF-8?q?fix:=20=E7=B5=B1=E4=B8=80=20handleTrade?= =?UTF-8?q?rList=20=E8=BF=94=E5=9B=9E=E5=AE=8C=E6=95=B4=20AI=20model=20ID?= =?UTF-8?q?=EF=BC=88=E4=BF=9D=E6=8C=81=E8=88=87=20handleGetTraderConfig=20?= =?UTF-8?q?=E4=B8=80=E8=87=B4=EF=BC=89=20=E5=95=8F=E9=A1=8C=EF=BC=9A=20-?= =?UTF-8?q?=20handleTraderList=20=E4=BB=8D=E5=9C=A8=E6=88=AA=E6=96=B7=20AI?= =?UTF-8?q?=20model=20ID=20(admin=5Fdeepseek=20=E2=86=92=20deepseek)=20-?= =?UTF-8?q?=20=E8=88=87=20handleGetTraderConfig=20=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E7=9A=84=E5=AE=8C=E6=95=B4=20ID=20=E4=B8=8D=E4=B8=80=E8=87=B4?= =?UTF-8?q?=20-=20=E5=B0=8E=E8=87=B4=E5=89=8D=E7=AB=AF=20isModelInUse=20?= =?UTF-8?q?=E6=AA=A2=E6=9F=A5=E5=A4=B1=E6=95=88=20=E4=BF=AE=E5=BE=A9?= =?UTF-8?q?=EF=BC=9A=20-=20=E7=A7=BB=E9=99=A4=20handleTraderList=20?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=88=AA=E6=96=B7=E9=82=8F=E8=BC=AF=20-=20?= =?UTF-8?q?=E8=BF=94=E5=9B=9E=E5=AE=8C=E6=95=B4=20AIModelID=20(admin=5Fdee?= =?UTF-8?q?pseek)=20-=20=E8=88=87=E5=85=B6=E4=BB=96=20API=20=E7=AB=AF?= =?UTF-8?q?=E9=BB=9E=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4=20=E6=B8=AC?= =?UTF-8?q?=E8=A9=A6=EF=BC=9A=20-=20GET=20/api/traders=20=E2=86=92=20ai=5F?= =?UTF-8?q?model:=20admin=5Fdeepseek=20=E2=9C=93=20-=20GET=20/api/traders/?= =?UTF-8?q?:id=20=E2=86=92=20ai=5Fmodel:=20admin=5Fdeepseek=20=E2=9C=93=20?= =?UTF-8?q?-=20=E6=A8=A1=E5=9E=8B=E4=BD=BF=E7=94=A8=E6=AA=A2=E6=9F=A5?= =?UTF-8?q?=E9=82=8F=E8=BC=AF=E6=AD=A3=E7=A2=BA=20=E2=9C=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/api/server.go b/api/server.go index 94ae4a60..0098a7a3 100644 --- a/api/server.go +++ b/api/server.go @@ -791,19 +791,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, From dc44bc9a1bc9a78f712bf3806c25ef6fc7dd2fe1 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Mon, 3 Nov 2025 00:56:02 +0800 Subject: [PATCH 029/195] =?UTF-8?q?chore:=20upgrade=20sqlite3=20to=20v1.14?= =?UTF-8?q?.22=20for=20Alpine=20Linux=20compatibility=20-=20Fix=20compilat?= =?UTF-8?q?ion=20error=20on=20Alpine:=20off64=5Ft=20type=20not=20defined?= =?UTF-8?q?=20in=20v1.14.16=20-=20Remove=20unused=20pure-Go=20sqlite=20imp?= =?UTF-8?q?lementation=20(modernc.org/sqlite)=20and=20its=20dependencies?= =?UTF-8?q?=20-=20v1.14.22=20is=20the=20first=20version=20fixing=20Alpine/?= =?UTF-8?q?musl=20build=20issues=20(2024-02-02)=20-=20Minimizes=20version?= =?UTF-8?q?=20jump=20(v1.14.16=20=E2=86=92=20v1.14.22,=2018=20commits)=20t?= =?UTF-8?q?o=20reduce=20risk=20Reference:=20https://github.com/mattn/go-sq?= =?UTF-8?q?lite3/issues/1164=20Verified:=20Builds=20successfully=20on=20go?= =?UTF-8?q?lang:1.25-alpine?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 72291ee0..5a9d3e60 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ require ( 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/mattn/go-sqlite3 v1.14.16 + github.com/mattn/go-sqlite3 v1.14.22 github.com/pquerna/otp v1.4.0 github.com/sonirico/go-hyperliquid v0.17.0 golang.org/x/crypto v0.42.0 diff --git a/go.sum b/go.sum index 655fcf92..d31c93fc 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= From 1e8746e6921078546291cf6f5c0f0c6d7e968310 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 17:39:00 +0800 Subject: [PATCH 030/195] chore: run go fmt to fix formatting issues --- api/server.go | 49 +++++++++++++++++++-------------------- config/database.go | 24 +++++++++---------- main.go | 22 +++++++++--------- manager/trader_manager.go | 40 ++++++++++++++++---------------- mcp/client.go | 4 ++-- 5 files changed, 69 insertions(+), 70 deletions(-) diff --git a/api/server.go b/api/server.go index 0098a7a3..d83e3887 100644 --- a/api/server.go +++ b/api/server.go @@ -88,7 +88,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) @@ -168,7 +168,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" @@ -531,14 +531,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 +574,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": "交易员不存在"}) @@ -796,7 +796,7 @@ func (s *Server) handleTraderList(c *gin.Context) { result = append(result, map[string]interface{}{ "trader_id": trader.ID, "trader_name": trader.Name, - "ai_model": trader.AIModelID, // 使用完整 ID + "ai_model": trader.AIModelID, // 使用完整 ID "exchange_id": trader.ExchangeID, "is_running": isRunning, "initial_balance": trader.InitialBalance, @@ -1574,7 +1574,7 @@ func (s *Server) handlePublicCompetition(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, competition) } @@ -1587,7 +1587,7 @@ func (s *Server) handleTopTraders(c *gin.Context) { }) return } - + c.JSON(http.StatusOK, topTraders) } @@ -1596,7 +1596,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请求) @@ -1610,13 +1610,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 { @@ -1624,24 +1624,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) } @@ -1651,31 +1651,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, @@ -1683,16 +1683,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 } @@ -1726,4 +1726,3 @@ func (s *Server) handleGetPublicTraderConfig(c *gin.Context) { c.JSON(http.StatusOK, result) } - diff --git a/config/database.go b/config/database.go index 651c425d..052a52ff 100644 --- a/config/database.go +++ b/config/database.go @@ -258,17 +258,17 @@ func (d *Database) initDefaultData() error { // 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新 systemConfigs := map[string]string{ - "admin_mode": "true", // 默认开启管理员模式,便于首次使用 - "beta_mode": "false", // 默认关闭内测模式 - "api_server_port": "8080", // 默认API端口 - "use_default_coins": "true", // 默认使用内置币种列表 - "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) - "max_daily_loss": "10.0", // 最大日损失百分比 - "max_drawdown": "20.0", // 最大回撤百分比 - "stop_trading_minutes": "60", // 停止交易时间(分钟) - "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 - "altcoin_leverage": "5", // 山寨币杠杆倍数 - "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 + "admin_mode": "true", // 默认开启管理员模式,便于首次使用 + "beta_mode": "false", // 默认关闭内测模式 + "api_server_port": "8080", // 默认API端口 + "use_default_coins": "true", // 默认使用内置币种列表 + "default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式) + "max_daily_loss": "10.0", // 最大日损失百分比 + "max_drawdown": "20.0", // 最大回撤百分比 + "stop_trading_minutes": "60", // 停止交易时间(分钟) + "btc_eth_leverage": "5", // BTC/ETH杠杆倍数 + "altcoin_leverage": "5", // 山寨币杠杆倍数 + "jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成 } for key, value := range systemConfigs { @@ -1037,7 +1037,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/main.go b/main.go index 8aa83dde..9e9d1aa7 100644 --- a/main.go +++ b/main.go @@ -64,15 +64,15 @@ func syncConfigToDatabase(database *config.Database) error { // 同步各配置项到数据库 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 +112,7 @@ func syncConfigToDatabase(database *config.Database) error { // loadBetaCodesToDatabase 加载内测码文件到数据库 func loadBetaCodesToDatabase(database *config.Database) error { betaCodeFile := "beta_codes.txt" - + // 检查内测码文件是否存在 if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) { log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile) @@ -126,7 +126,7 @@ func loadBetaCodesToDatabase(database *config.Database) error { } log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024) - + // 加载内测码到数据库 err = database.LoadBetaCodesFromFile(betaCodeFile) if err != nil { diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 4ebcf20b..ccb68bf0 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管理器 @@ -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), diff --git a/mcp/client.go b/mcp/client.go index 5ef090e8..aa1d5435 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -280,8 +280,8 @@ func isRetryableError(err error) bool { "connection refused", "temporary failure", "no such host", - "stream error", // HTTP/2 stream 错误 - "INTERNAL_ERROR", // 服务端内部错误 + "stream error", // HTTP/2 stream 错误 + "INTERNAL_ERROR", // 服务端内部错误 } for _, retryable := range retryableErrors { if strings.Contains(errStr, retryable) { From 4e6b8685311ee85d4233b3168db1a28f4f2f91f4 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:44:07 +0800 Subject: [PATCH 031/195] =?UTF-8?q?fix(margin):=20correct=20position=20siz?= =?UTF-8?q?ing=20formula=20to=20prevent=20insufficient=20margin=20errors?= =?UTF-8?q?=20##=20Problem=20AI=20was=20calculating=20position=5Fsize=5Fus?= =?UTF-8?q?d=20incorrectly,=20treating=20it=20as=20margin=20requirement=20?= =?UTF-8?q?instead=20of=20notional=20value,=20causing=20code=3D-2019=20err?= =?UTF-8?q?ors=20(insufficient=20margin).=20##=20Solution=20###=201.=20Upd?= =?UTF-8?q?ated=20AI=20prompts=20with=20correct=20formula=20-=20**prompts/?= =?UTF-8?q?adaptive.txt**:=20Added=20clear=20position=20sizing=20calculati?= =?UTF-8?q?on=20steps=20-=20**prompts/nof1.txt**:=20Added=20English=20vers?= =?UTF-8?q?ion=20with=20example=20-=20**prompts/default.txt**:=20Added=20C?= =?UTF-8?q?hinese=20version=20with=20example=20**Correct=20formula:**=201.?= =?UTF-8?q?=20Available=20Margin=20=3D=20Available=20Cash=20=C3=97=200.95?= =?UTF-8?q?=20=C3=97=20Allocation=20%=20(reserve=205%=20for=20fees)=202.?= =?UTF-8?q?=20Notional=20Value=20=3D=20Available=20Margin=20=C3=97=20Lever?= =?UTF-8?q?age=203.=20position=5Fsize=5Fusd=20=3D=20Notional=20Value=20(th?= =?UTF-8?q?is=20is=20the=20value=20for=20JSON)=20**Example:**=20$500=20cas?= =?UTF-8?q?h,=205x=20leverage=20=E2=86=92=20position=5Fsize=5Fusd=20=3D=20?= =?UTF-8?q?$2,375=20(not=20$500)=20###=202.=20Added=20code-level=20validat?= =?UTF-8?q?ion=20-=20**trader/auto=5Ftrader.go**:=20Added=20margin=20check?= =?UTF-8?q?s=20in=20executeOpenLong/ShortWithRecord=20-=20Validates=20requ?= =?UTF-8?q?ired=20margin=20+=20fees=20=E2=89=A4=20available=20balance=20be?= =?UTF-8?q?fore=20opening=20position=20-=20Returns=20clear=20error=20messa?= =?UTF-8?q?ge=20if=20insufficient=20##=20Impact=20-=20Prevents=20code=3D-2?= =?UTF-8?q?019=20errors=20-=20AI=20now=20understands=20the=20difference=20?= =?UTF-8?q?between=20notional=20value=20and=20margin=20requirement=20-=20D?= =?UTF-8?q?ouble=20validation:=20AI=20prompt=20+=20code=20check=20##=20Tes?= =?UTF-8?q?ting=20-=20=E2=9C=85=20Compiles=20successfully=20-=20=E2=9A=A0?= =?UTF-8?q?=EF=B8=8F=20Requires=20live=20trading=20environment=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prompts/adaptive.txt | 31 +++++++++++++++---------------- prompts/default.txt | 15 +++++++++++++++ prompts/nof1.txt | 15 ++++++++++++--- trader/auto_trader.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index d5778caa..e02c59f8 100644 --- a/prompts/adaptive.txt +++ b/prompts/adaptive.txt @@ -330,26 +330,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/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..e57efbec 100644 --- a/prompts/nof1.txt +++ b/prompts/nof1.txt @@ -45,10 +45,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/trader/auto_trader.go b/trader/auto_trader.go index 1e93ab5c..a598bde8 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -626,6 +626,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 +706,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) From 21ae77e7cc6717b9fb6c1cda939ee7c89525051b Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:58:20 +0800 Subject: [PATCH 032/195] =?UTF-8?q?fix(stats):=20aggregate=20partial=20clo?= =?UTF-8?q?ses=20into=20single=20trade=20for=20accurate=20statistics=20##?= =?UTF-8?q?=20Problem=20Multiple=20partial=5Fclose=20actions=20on=20the=20?= =?UTF-8?q?same=20position=20were=20being=20counted=20as=20separate=20trad?= =?UTF-8?q?es,=20inflating=20TotalTrades=20count=20and=20distorting=20win?= =?UTF-8?q?=20rate/profit=20factor=20statistics.=20**Example=20of=20bug:**?= =?UTF-8?q?=20-=20Open=201=20BTC=20@=20$100,000=20-=20Partial=20close=2030?= =?UTF-8?q?%=20@=20$101,000=20=E2=86=92=20Counted=20as=20trade=20#1=20?= =?UTF-8?q?=E2=9D=8C=20-=20Partial=20close=2050%=20@=20$102,000=20?= =?UTF-8?q?=E2=86=92=20Counted=20as=20trade=20#2=20=E2=9D=8C=20-=20Close?= =?UTF-8?q?=20remaining=2020%=20@=20$103,000=20=E2=86=92=20Counted=20as=20?= =?UTF-8?q?trade=20#3=20=E2=9D=8C=20-=20**Result:**=203=20trades=20instead?= =?UTF-8?q?=20of=201=20=E2=9D=8C=20##=20Solution=20###=201.=20Added=20trac?= =?UTF-8?q?king=20fields=20to=20openPositions=20map=20-=20`remainingQuanti?= =?UTF-8?q?ty`:=20Tracks=20remaining=20position=20size=20-=20`accumulatedP?= =?UTF-8?q?nL`:=20Accumulates=20PnL=20from=20all=20partial=20closes=20-=20?= =?UTF-8?q?`partialCloseCount`:=20Counts=20number=20of=20partial=20close?= =?UTF-8?q?=20operations=20-=20`partialCloseVolume`:=20Total=20volume=20cl?= =?UTF-8?q?osed=20partially=20###=202.=20Modified=20partial=5Fclose=20hand?= =?UTF-8?q?ling=20logic=20-=20Each=20partial=5Fclose:=20=20=20-=20Accumula?= =?UTF-8?q?tes=20PnL=20into=20`accumulatedPnL`=20=20=20-=20Reduces=20`rema?= =?UTF-8?q?iningQuantity`=20=20=20-=20**Does=20NOT=20increment=20TotalTrad?= =?UTF-8?q?es++**=20=20=20-=20Keeps=20position=20in=20openPositions=20map?= =?UTF-8?q?=20-=20Only=20when=20`remainingQuantity=20<=3D=200.0001`:=20=20?= =?UTF-8?q?=20-=20Records=20ONE=20TradeOutcome=20with=20aggregated=20PnL?= =?UTF-8?q?=20=20=20-=20Increments=20TotalTrades++=20once=20=20=20-=20Remo?= =?UTF-8?q?ves=20from=20openPositions=20map=20###=203.=20Updated=20full=20?= =?UTF-8?q?close=20handling=20-=20If=20position=20had=20prior=20partial=20?= =?UTF-8?q?closes:=20=20=20-=20Adds=20`accumulatedPnL`=20to=20final=20clos?= =?UTF-8?q?e=20PnL=20=20=20-=20Reports=20total=20PnL=20in=20TradeOutcome?= =?UTF-8?q?=20###=204.=20Fixed=20GetStatistics()=20-=20Removed=20`partial?= =?UTF-8?q?=5Fclose`=20from=20TotalClosePositions=20count=20-=20Only=20`cl?= =?UTF-8?q?ose=5Flong/close=5Fshort/auto=5Fclose`=20count=20as=20close=20o?= =?UTF-8?q?perations=20##=20Impact=20-=20=E2=9C=85=20Statistics=20now=20ac?= =?UTF-8?q?curate:=20multiple=20partial=20closes=20=3D=201=20trade=20-=20?= =?UTF-8?q?=E2=9C=85=20Win=20rate=20calculated=20correctly=20-=20=E2=9C=85?= =?UTF-8?q?=20Profit=20factor=20reflects=20true=20performance=20-=20?= =?UTF-8?q?=E2=9C=85=20Backward=20compatible:=20handles=20positions=20with?= =?UTF-8?q?out=20tracking=20fields=20##=20Testing=20-=20=E2=9C=85=20Compil?= =?UTF-8?q?es=20successfully=20-=20=E2=9A=A0=EF=B8=8F=20Requires=20validat?= =?UTF-8?q?ion=20with=20live=20partial=5Fclose=20scenarios=20##=20Code=20C?= =?UTF-8?q?hanges=20```=20logger/decision=5Flogger.go:=20-=20Lines=20420-4?= =?UTF-8?q?30:=20Add=20tracking=20fields=20to=20openPositions=20-=20Lines?= =?UTF-8?q?=20441-534:=20Implement=20partial=5Fclose=20aggregation=20logic?= =?UTF-8?q?=20-=20Lines=20536-593:=20Update=20full=20close=20to=20include?= =?UTF-8?q?=20accumulated=20PnL=20-=20Lines=20246-250:=20Fix=20GetStatisti?= =?UTF-8?q?cs()=20to=20exclude=20partial=5Fclose=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- logger/decision_logger.go | 255 ++++++++++++++++++++++++++++---------- 1 file changed, 187 insertions(+), 68 deletions(-) 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) + } } } } From 841cd2d277ef44574632087469efa88f38816879 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:00:23 +0800 Subject: [PATCH 033/195] =?UTF-8?q?fix(ui):=20prevent=20system=5Fprompt=5F?= =?UTF-8?q?template=20overwrite=20when=20value=20is=20empty=20string=20##?= =?UTF-8?q?=20Problem=20When=20editing=20trader=20configuration,=20if=20`s?= =?UTF-8?q?ystem=5Fprompt=5Ftemplate`=20was=20set=20to=20an=20empty=20stri?= =?UTF-8?q?ng=20(""),=20the=20UI=20would=20incorrectly=20treat=20it=20as?= =?UTF-8?q?=20falsy=20and=20overwrite=20it=20with=20'default',=20losing=20?= =?UTF-8?q?the=20user's=20selection.=20**Root=20cause:**=20```tsx=20if=20(?= =?UTF-8?q?traderData=20&&=20!traderData.system=5Fprompt=5Ftemplate)=20{?= =?UTF-8?q?=20=20=20//=20=E2=9D=8C=20This=20triggers=20for=20both=20undefi?= =?UTF-8?q?ned=20AND=20empty=20string=20""=20=20=20setFormData({=20system?= =?UTF-8?q?=5Fprompt=5Ftemplate:=20'default'=20});=20}=20```=20JavaScript?= =?UTF-8?q?=20falsy=20values=20that=20trigger=20`!`=20operator:=20-=20`und?= =?UTF-8?q?efined`=20=E2=9C=85=20Should=20trigger=20default=20-=20`null`?= =?UTF-8?q?=20=E2=9C=85=20Should=20trigger=20default=20-=20`""`=20?= =?UTF-8?q?=E2=9D=8C=20Should=20NOT=20trigger=20(user=20explicitly=20chose?= =?UTF-8?q?=20empty)=20-=20`false`,=20`0`,=20`NaN`=20(less=20relevant=20he?= =?UTF-8?q?re)=20##=20Solution=20Change=20condition=20to=20explicitly=20ch?= =?UTF-8?q?eck=20for=20`undefined`:=20```tsx=20if=20(traderData=20&&=20tra?= =?UTF-8?q?derData.system=5Fprompt=5Ftemplate=20=3D=3D=3D=20undefined)=20{?= =?UTF-8?q?=20=20=20//=20=E2=9C=85=20Only=20triggers=20for=20truly=20missi?= =?UTF-8?q?ng=20field=20=20=20setFormData({=20system=5Fprompt=5Ftemplate:?= =?UTF-8?q?=20'default'=20});=20}=20```=20##=20Impact=20-=20=E2=9C=85=20Em?= =?UTF-8?q?pty=20string=20selections=20are=20preserved=20-=20=E2=9C=85=20L?= =?UTF-8?q?egacy=20data=20(undefined)=20still=20gets=20default=20value=20-?= =?UTF-8?q?=20=E2=9C=85=20User's=20explicit=20choices=20are=20respected=20?= =?UTF-8?q?-=20=E2=9C=85=20No=20breaking=20changes=20to=20existing=20funct?= =?UTF-8?q?ionality=20##=20Testing=20-=20=E2=9C=85=20Code=20compiles=20-?= =?UTF-8?q?=20=E2=9A=A0=EF=B8=8F=20Requires=20manual=20UI=20testing:=20=20?= =?UTF-8?q?=20-=20[=20]=20Edit=20trader=20with=20empty=20system=5Fprompt?= =?UTF-8?q?=5Ftemplate=20=20=20-=20[=20]=20Verify=20it=20doesn't=20reset?= =?UTF-8?q?=20to=20'default'=20=20=20-=20[=20]=20Create=20new=20trader=20?= =?UTF-8?q?=E2=86=92=20should=20default=20to=20'default'=20=20=20-=20[=20]?= =?UTF-8?q?=20Edit=20old=20trader=20(undefined=20field)=20=E2=86=92=20shou?= =?UTF-8?q?ld=20default=20to=20'default'=20##=20Code=20Changes=20```=20web?= =?UTF-8?q?/src/components/TraderConfigModal.tsx:=20-=20Line=2099:=20Chang?= =?UTF-8?q?ed=20!traderData.system=5Fprompt=5Ftemplate=20=E2=86=92=20=3D?= =?UTF-8?q?=3D=3D=20undefined=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/TraderConfigModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/TraderConfigModal.tsx b/web/src/components/TraderConfigModal.tsx index 4676c194..4f9ddc72 100644 --- a/web/src/components/TraderConfigModal.tsx +++ b/web/src/components/TraderConfigModal.tsx @@ -96,7 +96,7 @@ export function TraderConfigModal({ }); } // 确保旧数据也有默认的 system_prompt_template - if (traderData && !traderData.system_prompt_template) { + if (traderData && traderData.system_prompt_template === undefined) { setFormData(prev => ({ ...prev, system_prompt_template: 'default' From 616c3508352f236b84ea9a72e281f3b61406fdbc Mon Sep 17 00:00:00 2001 From: sue <177699783@qq.com> Date: Tue, 4 Nov 2025 18:42:21 +0800 Subject: [PATCH 034/195] =?UTF-8?q?fix(trader):=20add=20missing=20Hyperliq?= =?UTF-8?q?uidTestnet=20configuration=20in=20loadSingleTrader=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BA=86=20loadSingleTrader=20=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E4=B8=AD=E7=BC=BA=E5=A4=B1=E7=9A=84=20HyperliquidTestnet=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B9=EF=BC=8C=20=E7=A1=AE=E4=BF=9D=20Hyp?= =?UTF-8?q?erliquid=20=E4=BA=A4=E6=98=93=E6=89=80=E7=9A=84=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=BD=91=E9=85=8D=E7=BD=AE=E8=83=BD=E5=A4=9F=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E4=BC=A0=E9=80=92=E5=88=B0=20trader=20=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E3=80=82=20Changes:=20-=20=E5=9C=A8=20loadSingleTrade?= =?UTF-8?q?r=20=E4=B8=AD=E6=B7=BB=E5=8A=A0=20HyperliquidTestnet=20?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E9=85=8D=E7=BD=AE=20-=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E4=BC=98=E5=8C=96=EF=BC=88=E7=A9=BA=E6=A0=BC?= =?UTF-8?q?=E5=AF=B9=E9=BD=90=EF=BC=89=20Co-Authored-By:=20tinkle-communit?= =?UTF-8?q?y=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- manager/trader_manager.go | 41 ++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 4ebcf20b..0fce9249 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管理器 @@ -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), @@ -889,6 +889,7 @@ func (tm *TraderManager) loadSingleTrader(traderCfg *config.TraderRecord, aiMode DefaultCoins: defaultCoins, TradingCoins: tradingCoins, SystemPromptTemplate: traderCfg.SystemPromptTemplate, // 系统提示词模板 + HyperliquidTestnet: exchangeCfg.Testnet, // Hyperliquid测试网 } // 根据交易所类型设置API密钥 From c9d5aed1b6a45b681a9faa1faaae96070a1e4b95 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:05:54 +0800 Subject: [PATCH 035/195] =?UTF-8?q?fix(trader):=20separate=20stop-loss=20a?= =?UTF-8?q?nd=20take-profit=20order=20cancellation=20to=20prevent=20accide?= =?UTF-8?q?ntal=20deletions=20##=20Problem=20When=20adjusting=20stop-loss?= =?UTF-8?q?=20or=20take-profit=20levels,=20`CancelStopOrders()`=20deleted?= =?UTF-8?q?=20BOTH=20stop-loss=20AND=20take-profit=20orders=20simultaneous?= =?UTF-8?q?ly,=20causing:=20-=20**Adjusting=20stop-loss**=20=E2=86=92=20Ta?= =?UTF-8?q?ke-profit=20order=20deleted=20=E2=86=92=20Position=20has=20no?= =?UTF-8?q?=20exit=20plan=20=E2=9D=8C=20-=20**Adjusting=20take-profit**=20?= =?UTF-8?q?=E2=86=92=20Stop-loss=20order=20deleted=20=E2=86=92=20Position?= =?UTF-8?q?=20unprotected=20=E2=9D=8C=20**Root=20cause:**=20```go=20Cancel?= =?UTF-8?q?StopOrders(symbol)=20{=20=20=20//=20Cancelled=20ALL=20orders=20?= =?UTF-8?q?with=20type=20STOP=5FMARKET=20or=20TAKE=5FPROFIT=5FMARKET=20=20?= =?UTF-8?q?=20//=20No=20distinction=20between=20stop-loss=20and=20take-pro?= =?UTF-8?q?fit=20}=20```=20##=20Solution=20###=201.=20Added=20new=20interf?= =?UTF-8?q?ace=20methods=20(trader/interface.go)=20```go=20CancelStopLossO?= =?UTF-8?q?rders(symbol=20string)=20error=20=20=20=20=20=20//=20Only=20can?= =?UTF-8?q?cel=20stop-loss=20orders=20CancelTakeProfitOrders(symbol=20stri?= =?UTF-8?q?ng)=20error=20=20=20=20//=20Only=20cancel=20take-profit=20order?= =?UTF-8?q?s=20CancelStopOrders(symbol=20string)=20error=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20=20//=20Deprecated=20(cancels=20both)=20```=20###=202.?= =?UTF-8?q?=20Implemented=20for=20all=203=20exchanges=20**Binance=20(trade?= =?UTF-8?q?r/binance=5Ffutures.go)**:=20-=20`CancelStopLossOrders`:=20Filt?= =?UTF-8?q?ers=20`OrderTypeStopMarket=20|=20OrderTypeStop`=20-=20`CancelTa?= =?UTF-8?q?keProfitOrders`:=20Filters=20`OrderTypeTakeProfitMarket=20|=20O?= =?UTF-8?q?rderTypeTakeProfit`=20-=20Full=20order=20type=20differentiation?= =?UTF-8?q?=20=E2=9C=85=20**Hyperliquid=20(trader/hyperliquid=5Ftrader.go)?= =?UTF-8?q?**:=20-=20=E2=9A=A0=EF=B8=8F=20Limitation:=20SDK's=20OpenOrder?= =?UTF-8?q?=20struct=20doesn't=20expose=20trigger=20field=20-=20Both=20met?= =?UTF-8?q?hods=20call=20`CancelStopOrders`=20(cancels=20all=20pending=20o?= =?UTF-8?q?rders)=20-=20Trade-off:=20Safe=20but=20less=20precise=20**Aster?= =?UTF-8?q?=20(trader/aster=5Ftrader.go)**:=20-=20`CancelStopLossOrders`:?= =?UTF-8?q?=20Filters=20`STOP=5FMARKET=20|=20STOP`=20-=20`CancelTakeProfit?= =?UTF-8?q?Orders`:=20Filters=20`TAKE=5FPROFIT=5FMARKET=20|=20TAKE=5FPROFI?= =?UTF-8?q?T`=20-=20Full=20order=20type=20differentiation=20=E2=9C=85=20##?= =?UTF-8?q?#=203.=20Usage=20in=20auto=5Ftrader.go=20When=20`update=5Fstop?= =?UTF-8?q?=5Floss`=20or=20`update=5Ftake=5Fprofit`=20actions=20are=20impl?= =?UTF-8?q?emented,=20they=20will=20use:=20```go=20//=20update=5Fstop=5Flo?= =?UTF-8?q?ss:=20at.trader.CancelStopLossOrders(symbol)=20=20//=20Only=20c?= =?UTF-8?q?ancel=20SL,=20keep=20TP=20at.trader.SetStopLoss(...)=20//=20upd?= =?UTF-8?q?ate=5Ftake=5Fprofit:=20at.trader.CancelTakeProfitOrders(symbol)?= =?UTF-8?q?=20=20//=20Only=20cancel=20TP,=20keep=20SL=20at.trader.SetTakeP?= =?UTF-8?q?rofit(...)=20```=20##=20Impact=20-=20=E2=9C=85=20Adjusting=20st?= =?UTF-8?q?op-loss=20no=20longer=20deletes=20take-profit=20-=20=E2=9C=85?= =?UTF-8?q?=20Adjusting=20take-profit=20no=20longer=20deletes=20stop-loss?= =?UTF-8?q?=20-=20=E2=9C=85=20Backward=20compatible:=20`CancelStopOrders`?= =?UTF-8?q?=20still=20exists=20(deprecated)=20-=20=E2=9A=A0=EF=B8=8F=20Hyp?= =?UTF-8?q?erliquid=20limitation:=20still=20cancels=20all=20orders=20(SDK?= =?UTF-8?q?=20constraint)=20##=20Testing=20-=20=E2=9C=85=20Compiles=20succ?= =?UTF-8?q?essfully=20across=20all=203=20exchanges=20-=20=E2=9A=A0?= =?UTF-8?q?=EF=B8=8F=20Requires=20live=20testing:=20=20=20-=20[=20]=20Bina?= =?UTF-8?q?nce:=20Adjust=20SL=20=E2=86=92=20verify=20TP=20remains=20=20=20?= =?UTF-8?q?-=20[=20]=20Binance:=20Adjust=20TP=20=E2=86=92=20verify=20SL=20?= =?UTF-8?q?remains=20=20=20-=20[=20]=20Hyperliquid:=20Verify=20behavior=20?= =?UTF-8?q?with=20limitation=20=20=20-=20[=20]=20Aster:=20Verify=20order?= =?UTF-8?q?=20filtering=20works=20correctly=20##=20Code=20Changes=20```=20?= =?UTF-8?q?trader/interface.go:=20+9=20lines=20(new=20interface=20methods)?= =?UTF-8?q?=20trader/binance=5Ffutures.go:=20+133=20lines=20(3=20new=20fun?= =?UTF-8?q?ctions)=20trader/hyperliquid=5Ftrader.go:=20+56=20lines=20(3=20?= =?UTF-8?q?new=20functions)=20trader/aster=5Ftrader.go:=20+157=20lines=20(?= =?UTF-8?q?3=20new=20functions)=20Total:=20+355=20lines=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/aster_trader.go | 155 +++++++++++++++++++++++++++++++++++ trader/binance_futures.go | 131 +++++++++++++++++++++++++++++ trader/hyperliquid_trader.go | 50 +++++++++++ trader/interface.go | 10 +++ 4 files changed, 346 insertions(+) diff --git a/trader/aster_trader.go b/trader/aster_trader.go index d9ba82a6..f0cd9f9a 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -971,6 +971,161 @@ func (t *AsterTrader) SetTakeProfit(symbol string, positionSide string, quantity 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 +} + +// 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{}{ diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..e5aea02a 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -411,6 +411,137 @@ func (t *FuturesTrader) CloseShort(symbol string, quantity float64) (map[string] return result, 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 +} + +// 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(). diff --git a/trader/hyperliquid_trader.go b/trader/hyperliquid_trader.go index c189dbdc..d7884259 100644 --- a/trader/hyperliquid_trader.go +++ b/trader/hyperliquid_trader.go @@ -477,6 +477,56 @@ func (t *HyperliquidTrader) CloseShort(symbol string, quantity float64) (map[str return result, 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 +} + +// 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) diff --git a/trader/interface.go b/trader/interface.go index 18d75ee7..660b09b9 100644 --- a/trader/interface.go +++ b/trader/interface.go @@ -36,6 +36,16 @@ type Trader interface { // SetTakeProfit 设置止盈单 SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error + // CancelStopOrders 取消该币种的止盈/止损单(已废弃:会同时删除止损和止盈) + // 请使用 CancelStopLossOrders 或 CancelTakeProfitOrders + CancelStopOrders(symbol string) error + + // CancelStopLossOrders 仅取消止损单(修复 BUG:调整止损时不删除止盈) + CancelStopLossOrders(symbol string) error + + // CancelTakeProfitOrders 仅取消止盈单(修复 BUG:调整止盈时不删除止损) + CancelTakeProfitOrders(symbol string) error + // CancelAllOrders 取消该币种的所有挂单 CancelAllOrders(symbol string) error From 324ed50b9215f1c8ec8413015fb2412456e04b70 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:07:58 +0800 Subject: [PATCH 036/195] =?UTF-8?q?fix(binance):=20initialize=20dual-side?= =?UTF-8?q?=20position=20mode=20to=20prevent=20code=3D-4061=20errors=20##?= =?UTF-8?q?=20Problem=20When=20opening=20positions=20with=20explicit=20`Po?= =?UTF-8?q?sitionSide`=20parameter=20(LONG/SHORT),=20Binance=20API=20retur?= =?UTF-8?q?ned=20**code=3D-4061**=20error:=20```=20"No=20need=20to=20chang?= =?UTF-8?q?e=20position=20side."=20"code":-4061=20```=20**Root=20cause:**?= =?UTF-8?q?=20-=20Binance=20accounts=20default=20to=20**single-side=20posi?= =?UTF-8?q?tion=20mode**=20("One-Way=20Mode")=20-=20In=20this=20mode,=20`P?= =?UTF-8?q?ositionSide`=20parameter=20is=20**not=20allowed**=20-=20Code?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E4=BA=86=20`PositionSide`=20=E5=8F=83?= =?UTF-8?q?=E6=95=B8=20(LONG/SHORT)=EF=BC=8C=E4=BD=86=E5=B8=B3=E6=88=B6?= =?UTF-8?q?=E6=9C=AA=E5=95=9F=E7=94=A8=E9=9B=99=E5=90=91=E6=8C=81=E5=80=89?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=20**Position=20Mode=20Comparison:**=20|=20Mo?= =?UTF-8?q?de=20|=20PositionSide=20Required=20|=20Can=20Hold=20Long+Short?= =?UTF-8?q?=20Simultaneously=20|=20|------|----------------------|--------?= =?UTF-8?q?----------------------------|=20|=20One-Way=20(default)=20|=20?= =?UTF-8?q?=E2=9D=8C=20No=20|=20=E2=9D=8C=20No=20|=20|=20Hedge=20Mode=20|?= =?UTF-8?q?=20=E2=9C=85=20**Required**=20|=20=E2=9C=85=20Yes=20|=20##=20So?= =?UTF-8?q?lution=20###=201.=20Added=20setDualSidePosition()=20function=20?= =?UTF-8?q?Automatically=20enables=20Hedge=20Mode=20during=20trader=20init?= =?UTF-8?q?ialization:=20```go=20func=20(t=20*FuturesTrader)=20setDualSide?= =?UTF-8?q?Position()=20error=20{=20=20=20=20=20err=20:=3D=20t.client.NewC?= =?UTF-8?q?hangePositionModeService().=20=20=20=20=20=20=20=20=20DualSide(?= =?UTF-8?q?true).=20//=20Enable=20Hedge=20Mode=20=20=20=20=20=20=20=20=20D?= =?UTF-8?q?o(context.Background())=20=20=20=20=20if=20err=20!=3D=20nil=20{?= =?UTF-8?q?=20=20=20=20=20=20=20=20=20//=20Ignore=20"No=20need=20to=20chan?= =?UTF-8?q?ge"=20error=20(already=20in=20Hedge=20Mode)=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20if=20strings.Contains(err.Error(),=20"No=20need=20to?= =?UTF-8?q?=20change=20position=20side")=20{=20=20=20=20=20=20=20=20=20=20?= =?UTF-8?q?=20=20=20log.Printf("=E2=9C=93=20Account=20already=20in=20Hedge?= =?UTF-8?q?=20Mode")=20=20=20=20=20=20=20=20=20=20=20=20=20return=20nil=20?= =?UTF-8?q?=20=20=20=20=20=20=20=20}=20=20=20=20=20=20=20=20=20return=20er?= =?UTF-8?q?r=20=20=20=20=20}=20=20=20=20=20log.Printf("=E2=9C=93=20Switche?= =?UTF-8?q?d=20to=20Hedge=20Mode")=20=20=20=20=20return=20nil=20}=20```=20?= =?UTF-8?q?###=202.=20Called=20in=20NewFuturesTrader()=20Runs=20automatica?= =?UTF-8?q?lly=20when=20creating=20trader=20instance:=20```go=20func=20New?= =?UTF-8?q?FuturesTrader(apiKey,=20secretKey=20string)=20*FuturesTrader=20?= =?UTF-8?q?{=20=20=20=20=20trader=20:=3D=20&FuturesTrader{...}=20=20=20=20?= =?UTF-8?q?=20//=20Initialize=20Hedge=20Mode=20=20=20=20=20if=20err=20:=3D?= =?UTF-8?q?=20trader.setDualSidePosition();=20err=20!=3D=20nil=20{=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20log.Printf("=E2=9A=A0=EF=B8=8F=20Failed=20?= =?UTF-8?q?to=20set=20Hedge=20Mode:=20%v",=20err)=20=20=20=20=20}=20=20=20?= =?UTF-8?q?=20=20return=20trader=20}=20```=20##=20Impact=20-=20=E2=9C=85?= =?UTF-8?q?=20Prevents=20code=3D-4061=20errors=20when=20opening=20position?= =?UTF-8?q?s=20-=20=E2=9C=85=20Enables=20simultaneous=20long+short=20posit?= =?UTF-8?q?ions=20(if=20needed)=20-=20=E2=9C=85=20Fails=20gracefully=20if?= =?UTF-8?q?=20account=20already=20in=20Hedge=20Mode=20-=20=E2=9A=A0?= =?UTF-8?q?=EF=B8=8F=20**One-time=20change**:=20Once=20enabled,=20cannot?= =?UTF-8?q?=20revert=20to=20One-Way=20Mode=20with=20open=20positions=20##?= =?UTF-8?q?=20Testing=20-=20=E2=9C=85=20Compiles=20successfully=20-=20?= =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Requires=20Binance=20testnet/mainnet=20val?= =?UTF-8?q?idation:=20=20=20-=20[=20]=20First=20initialization=20=E2=86=92?= =?UTF-8?q?=20switches=20to=20Hedge=20Mode=20=20=20-=20[=20]=20Subsequent?= =?UTF-8?q?=20initializations=20=E2=86=92=20ignores=20"No=20need=20to=20ch?= =?UTF-8?q?ange"=20error=20=20=20-=20[=20]=20Open=20long=20position=20with?= =?UTF-8?q?=20PositionSide=3DLONG=20=E2=86=92=20succeeds=20=20=20-=20[=20]?= =?UTF-8?q?=20Open=20short=20position=20with=20PositionSide=3DSHORT=20?= =?UTF-8?q?=E2=86=92=20succeeds=20##=20Code=20Changes=20```=20trader/binan?= =?UTF-8?q?ce=5Ffutures.go:=20-=20Line=203-12:=20Added=20strings=20import?= =?UTF-8?q?=20-=20Line=2033-47:=20Modified=20NewFuturesTrader()=20to=20cal?= =?UTF-8?q?l=20setDualSidePosition()=20-=20Line=2049-69:=20New=20function?= =?UTF-8?q?=20setDualSidePosition()=20Total:=20+25=20lines=20```=20##=20Re?= =?UTF-8?q?ferences=20-=20Binance=20Futures=20API:=20https://binance-docs.?= =?UTF-8?q?github.io/apidocs/futures/en/#change-position-mode-trade=20-=20?= =?UTF-8?q?Error=20code=3D-4061:=20"No=20need=20to=20change=20position=20s?= =?UTF-8?q?ide."=20-=20PositionSide=20ENUM:=20BOTH=20(One-Way)=20|=20LONG?= =?UTF-8?q?=20|=20SHORT=20(Hedge=20Mode)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/binance_futures.go | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..5cffd96c 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,40 @@ type FuturesTrader struct { // NewFuturesTrader 创建合约交易器 func NewFuturesTrader(apiKey, secretKey string) *FuturesTrader { client := futures.NewClient(apiKey, secretKey) - return &FuturesTrader{ + 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 } // GetBalance 获取账户余额(带缓存) From f99052e78099bbae5b62f7ea7e6bc0dbe9c670ec Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:41:23 +0800 Subject: [PATCH 037/195] =?UTF-8?q?fix(prompts):=20rename=20actions=20to?= =?UTF-8?q?=20match=20backend=20implementation=20##=20Problem=20Backend=20?= =?UTF-8?q?code=20expects=20these=20action=20names:=20-=20`open=5Flong`,?= =?UTF-8?q?=20`open=5Fshort`,=20`close=5Flong`,=20`close=5Fshort`=20But=20?= =?UTF-8?q?prompts=20use=20outdated=20names:=20-=20`buy=5Fto=5Fenter`,=20`?= =?UTF-8?q?sell=5Fto=5Fenter`,=20`close`=20This=20causes=20all=20trading?= =?UTF-8?q?=20decisions=20to=20fail=20with=20unknown=20action=20errors.=20?= =?UTF-8?q?##=20Solution=20Minimal=20changes=20to=20fix=20action=20name=20?= =?UTF-8?q?compatibility:=20###=20prompts/nof1.txt=20-=20=E2=9C=85=20`buy?= =?UTF-8?q?=5Fto=5Fenter`=20=E2=86=92=20`open=5Flong`=20-=20=E2=9C=85=20`s?= =?UTF-8?q?ell=5Fto=5Fenter`=20=E2=86=92=20`open=5Fshort`=20-=20=E2=9C=85?= =?UTF-8?q?=20`close`=20=E2=86=92=20`close=5Flong`=20/=20`close=5Fshort`?= =?UTF-8?q?=20-=20=E2=9C=85=20Explicitly=20list=20`wait`=20action=20-=20+1?= =?UTF-8?q?8=20lines,=20-6=20lines=20(only=20action=20definitions=20sectio?= =?UTF-8?q?n)=20###=20prompts/adaptive.txt=20-=20=E2=9C=85=20`buy=5Fto=5Fe?= =?UTF-8?q?nter`=20=E2=86=92=20`open=5Flong`=20-=20=E2=9C=85=20`sell=5Fto?= =?UTF-8?q?=5Fenter`=20=E2=86=92=20`open=5Fshort`=20-=20=E2=9C=85=20`close?= =?UTF-8?q?`=20=E2=86=92=20`close=5Flong`=20/=20`close=5Fshort`=20-=20+15?= =?UTF-8?q?=20lines,=20-6=20lines=20(only=20action=20definitions=20section?= =?UTF-8?q?)=20##=20Impact=20-=20=E2=9C=85=20Trading=20decisions=20now=20e?= =?UTF-8?q?xecute=20successfully=20-=20=E2=9C=85=20Maintains=20all=20exist?= =?UTF-8?q?ing=20functionality=20-=20=E2=9C=85=20No=20new=20features=20add?= =?UTF-8?q?ed=20(minimal=20diff)=20##=20Verification=20```bash=20#=20Backe?= =?UTF-8?q?nd=20expects=20these=20actions:=20grep=20'Action=20string'=20de?= =?UTF-8?q?cision/engine.go=20#=20"open=5Flong",=20"open=5Fshort",=20"clos?= =?UTF-8?q?e=5Flong",=20"close=5Fshort",=20...=20#=20Old=20names=20removed?= =?UTF-8?q?:=20grep=20-r=20"buy=5Fto=5Fenter\|sell=5Fto=5Fenter"=20prompts?= =?UTF-8?q?/=20#=20(no=20results)=20```=20Co-Authored-By:=20tinkle-communi?= =?UTF-8?q?ty=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prompts/adaptive.txt | 15 +++++++++------ prompts/nof1.txt | 18 ++++++++++++------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/prompts/adaptive.txt b/prompts/adaptive.txt index d5778caa..3d9657f6 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**: 持有当前仓位 - 用于: 持仓表现符合预期,继续等待 ## 动态调整动作 (新增) diff --git a/prompts/nof1.txt b/prompts/nof1.txt index 012daa62..2e707b01 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 From c8f72bcc78a0591aaa85cc6aee0a3027e884e1a0 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 19:55:16 +0800 Subject: [PATCH 038/195] fix(api): add balance sync endpoint with smart detection ## Summary - Add POST /traders/:id/sync-balance endpoint (Option B) - Add smart detection showing balance change percentage (Option C) - Fix balance display bug caused by commit 2b9c4d2 ## Changes ### api/server.go - Add handleSyncBalance() handler - Query actual exchange balance via trader.GetBalance() - Calculate change percentage for smart detection - Update initial_balance in database - Reload trader into memory after update ### config/database.go - Add UpdateTraderInitialBalance() method - Update traders.initial_balance field ## Root Cause Commit 2b9c4d2 auto-queries exchange balance at trader creation time, but never updates after user deposits more funds, causing: - Wrong initial_balance (400 USDT vs actual 3000 USDT) - Wrong P&L calculations (-2598.55 USDT instead of actual) ## Solution Provides manual sync API + smart detection to update initial_balance when user deposits funds after trader creation. Co-Authored-By: tinkle-community --- api/server.go | 109 +++++++++++++++++++++++++++++++++++++++++++++ config/database.go | 6 +++ 2 files changed, 115 insertions(+) diff --git a/api/server.go b/api/server.go index 94ae4a60..eef3e031 100644 --- a/api/server.go +++ b/api/server.go @@ -9,6 +9,7 @@ import ( "nofx/config" "nofx/decision" "nofx/manager" + "nofx/trader" "strconv" "strings" "time" @@ -109,6 +110,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) @@ -641,6 +643,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") diff --git a/config/database.go b/config/database.go index 651c425d..84a3489d 100644 --- a/config/database.go +++ b/config/database.go @@ -853,6 +853,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) From 7091f76ca80198f99c128b456664d3ed04eef0cc Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 20:43:16 +0800 Subject: [PATCH 039/195] =?UTF-8?q?feat(trader):=20add=20automatic=20balan?= =?UTF-8?q?ce=20sync=20every=2010=20minutes=20##=20=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=20=E8=87=AA=E5=8A=A8=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E4=BA=A4=E6=98=93=E6=89=80=E4=BD=99=E9=A2=9D=E5=8F=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E6=97=A0=E9=9C=80=E7=94=A8=E6=88=B7=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E6=93=8D=E4=BD=9C=20##=20=E6=A0=B8=E5=BF=83=E6=94=B9=E5=8A=A8?= =?UTF-8?q?=201.=20AutoTrader=20=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=EF=BC=9A=20=20=20=20-=20lastBalanceSyncTime:=20=E4=B8=8A?= =?UTF-8?q?=E6=AC=A1=E4=BD=99=E9=A2=9D=E5=90=8C=E6=AD=A5=E6=97=B6=E9=97=B4?= =?UTF-8?q?=20=20=20=20-=20database:=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E5=BC=95=E7=94=A8=EF=BC=88=E7=94=A8=E4=BA=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=89=20=20=20=20-=20userID:=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7ID=202.=20=E6=96=B0=E5=A2=9E=E6=96=B9=E6=B3=95=20autoS?= =?UTF-8?q?yncBalanceIfNeeded():=20=20=20=20-=20=E6=AF=8F10=E5=88=86?= =?UTF-8?q?=E9=92=9F=E6=A3=80=E6=9F=A5=E4=B8=80=E6=AC=A1=EF=BC=88=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E4=B8=8E3=E5=88=86=E9=92=9F=E6=89=AB=E6=8F=8F?= =?UTF-8?q?=E5=91=A8=E6=9C=9F=E9=87=8D=E5=8F=A0=EF=BC=89=20=20=20=20-=20?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E5=8F=98=E5=8C=96>5%=E6=89=8D=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=95=B0=E6=8D=AE=E5=BA=93=20=20=20=20-=20=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E5=A4=B1=E8=B4=A5=E9=87=8D=E8=AF=95=EF=BC=88=E9=81=BF?= =?UTF-8?q?=E5=85=8D=E9=A2=91=E7=B9=81=E6=9F=A5=E8=AF=A2=EF=BC=89=20=20=20?= =?UTF-8?q?=20-=20=E5=AE=8C=E6=95=B4=E6=97=A5=E5=BF=97=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=203.=20=E9=9B=86=E6=88=90=E5=88=B0=E4=BA=A4=E6=98=93=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF:=20=20=20=20-=20=E5=9C=A8=20runCycle()=20=E4=B8=AD?= =?UTF-8?q?=E7=AC=AC3=E6=AD=A5=E8=87=AA=E5=8A=A8=E8=B0=83=E7=94=A8=20=20?= =?UTF-8?q?=20=20-=20=E5=85=88=E5=90=8C=E6=AD=A5=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=EF=BC=8C=E5=86=8D=E8=8E=B7=E5=8F=96=E4=BA=A4=E6=98=93=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=20=20=20=20-=20=E4=B8=8D=E5=BD=B1=E5=93=8D?= =?UTF-8?q?=E7=8E=B0=E6=9C=89=E4=BA=A4=E6=98=93=E9=80=BB=E8=BE=91=204.=20T?= =?UTF-8?q?raderManager=20=E6=9B=B4=E6=96=B0:=20=20=20=20-=20addTraderFrom?= =?UTF-8?q?DB(),=20AddTraderFromDB(),=20loadSingleTrader()=20=20=20=20-=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20database=20=E5=92=8C=20userID=20=E5=8F=82?= =?UTF-8?q?=E6=95=B0=20=20=20=20-=20=E6=AD=A3=E7=A1=AE=E4=BC=A0=E9=80=92?= =?UTF-8?q?=E5=88=B0=20NewAutoTrader()=205.=20Database=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=96=B9=E6=B3=95:=20=20=20=20-=20UpdateTraderInitial?= =?UTF-8?q?Balance(userID,=20id,=20newBalance)=20=20=20=20-=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E6=9B=B4=E6=96=B0=E5=88=9D=E5=A7=8B=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=20##=20=E4=B8=BA=E4=BB=80=E4=B9=88=E9=80=89=E6=8B=A910?= =?UTF-8?q?=E5=88=86=E9=92=9F=EF=BC=9F=201.=20=E9=81=BF=E5=85=8D=E4=B8=8E3?= =?UTF-8?q?=E5=88=86=E9=92=9F=E6=89=AB=E6=8F=8F=E5=91=A8=E6=9C=9F=E9=87=8D?= =?UTF-8?q?=E5=8F=A0=EF=BC=88=E6=AF=8F30=E5=88=86=E9=92=9F=E4=BB=85?= =?UTF-8?q?=E9=87=8D=E5=8F=A01=E6=AC=A1=EF=BC=89=202.=20API=E5=BC=80?= =?UTF-8?q?=E9=94=80=E6=9C=80=E5=B0=8F=E5=8C=96=EF=BC=9A=E6=AF=8F=E5=B0=8F?= =?UTF-8?q?=E6=97=B6=E4=BB=856=E6=AC=A1=E9=A2=9D=E5=A4=96=E8=B0=83?= =?UTF-8?q?=E7=94=A8=203.=20=E5=85=85=E5=80=BC=E5=BB=B6=E8=BF=9F=E5=8F=AF?= =?UTF-8?q?=E6=8E=A5=E5=8F=97=EF=BC=9A=E6=9C=80=E5=A4=9A10=E5=88=86?= =?UTF-8?q?=E9=92=9F=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=204.=20API?= =?UTF-8?q?=E5=8D=A0=E7=94=A8=E7=8E=87=EF=BC=9A0.2%=EF=BC=88=E8=BF=9C?= =?UTF-8?q?=E4=BD=8E=E4=BA=8E=E5=B8=81=E5=AE=892400=E6=AC=A1/=E5=88=86?= =?UTF-8?q?=E9=92=9F=E9=99=90=E5=88=B6=EF=BC=89=20##=20API=E5=BC=80?= =?UTF-8?q?=E9=94=80=20-=20GetBalance()=20=E8=BD=BB=E9=87=8F=E7=BA=A7?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=EF=BC=88=E6=9D=83=E9=87=8D5-10=EF=BC=89=20-?= =?UTF-8?q?=20=E6=AF=8F=E5=B0=8F=E6=97=B6=E4=BB=856=E6=AC=A1=E9=A2=9D?= =?UTF-8?q?=E5=A4=96=E8=B0=83=E7=94=A8=20-=20=E6=80=BB=E8=B0=83=E7=94=A8?= =?UTF-8?q?=EF=BC=9A26=E6=AC=A1/=E5=B0=8F=E6=97=B6=EF=BC=88runCycle:20=20+?= =?UTF-8?q?=20autoSync:6=EF=BC=89=20-=20=E5=8D=A0=E7=94=A8=E7=8E=87?= =?UTF-8?q?=EF=BC=9A(10/2400)/60=20=3D=200.2%=20=E2=9C=85=20##=20=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BD=93=E9=AA=8C=20-=20=E5=85=85=E5=80=BC=E5=90=8E?= =?UTF-8?q?=E6=9C=80=E5=A4=9A10=E5=88=86=E9=92=9F=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=20-=20=E5=AE=8C=E5=85=A8=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E6=97=A0=E9=9C=80=E6=89=8B=E5=8A=A8=E5=B9=B2?= =?UTF-8?q?=E9=A2=84=20-=20=E5=89=8D=E7=AB=AF=E6=95=B0=E6=8D=AE=E5=AE=9E?= =?UTF-8?q?=E6=97=B6=E5=87=86=E7=A1=AE=20##=20=E6=97=A5=E5=BF=97=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B=20-=20=F0=9F=94=84=20=E5=BC=80=E5=A7=8B=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=A3=80=E6=9F=A5=E4=BD=99=E9=A2=9D=E5=8F=98=E5=8C=96?= =?UTF-8?q?...=20-=20=F0=9F=94=94=20=E6=A3=80=E6=B5=8B=E5=88=B0=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=E5=A4=A7=E5=B9=85=E5=8F=98=E5=8C=96:=20693.00=20?= =?UTF-8?q?=E2=86=92=203693.00=20USDT=20(433.19%)=20-=20=E2=9C=85=20?= =?UTF-8?q?=E5=B7=B2=E8=87=AA=E5=8A=A8=E5=90=8C=E6=AD=A5=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=E5=88=B0=E6=95=B0=E6=8D=AE=E5=BA=93=20-=20=E2=9C=93=20?= =?UTF-8?q?=E4=BD=99=E9=A2=9D=E5=8F=98=E5=8C=96=E4=B8=8D=E5=A4=A7=20(2.3%)?= =?UTF-8?q?=EF=BC=8C=E6=97=A0=E9=9C=80=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/database.go | 6 +++ manager/trader_manager.go | 16 ++++---- trader/auto_trader.go | 82 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 11 deletions(-) diff --git a/config/database.go b/config/database.go index 651c425d..cffaabe9 100644 --- a/config/database.go +++ b/config/database.go @@ -853,6 +853,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) diff --git a/manager/trader_manager.go b/manager/trader_manager.go index 4ebcf20b..e3c3b400 100644 --- a/manager/trader_manager.go +++ b/manager/trader_manager.go @@ -170,7 +170,7 @@ func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) erro } // 添加到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.Database) erro } // 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.Database, 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) } @@ -832,7 +832,7 @@ func (tm *TraderManager) LoadUserTraders(database *config.Database, userID strin } // 使用现有的方法加载交易员 - 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.Database, userID strin } // 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.Database, userID string) error { // 处理交易币种列表 var tradingCoins []string if traderCfg.TradingSymbols != "" { @@ -912,7 +912,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/trader/auto_trader.go b/trader/auto_trader.go index 1e93ab5c..de7feda3 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "math" "nofx/decision" "nofx/logger" "nofx/market" @@ -98,10 +99,13 @@ type AutoTrader struct { startTime time.Time // 系统启动时间 callCount int // AI调用次数 positionFirstSeenTime map[string]int64 // 持仓首次出现时间 (symbol_side -> timestamp毫秒) + 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" @@ -216,6 +220,9 @@ func NewAutoTrader(config AutoTraderConfig) (*AutoTrader, error) { callCount: 0, isRunning: false, positionFirstSeenTime: make(map[string]int64), + lastBalanceSyncTime: time.Now(), // 初始化为当前时间 + database: database, + userID: userID, }, nil } @@ -253,6 +260,72 @@ func (at *AutoTrader) Stop() { 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 + 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] 余额变化不大 (%.2f%%),无需更新", at.name, changePercent) + } + + at.lastBalanceSyncTime = time.Now() +} + // runCycle 运行一个交易周期(使用AI全权决策) func (at *AutoTrader) runCycle() error { at.callCount++ @@ -284,7 +357,10 @@ func (at *AutoTrader) runCycle() error { log.Println("📅 日盈亏已重置") } - // 3. 收集交易上下文 + // 3. 自动同步余额(每10分钟检查一次,充值/提现后自动更新) + at.autoSyncBalanceIfNeeded() + + // 4. 收集交易上下文 ctx, err := at.buildTradingContext() if err != nil { record.Success = false @@ -324,7 +400,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) From 2bab17d0439660b75795bd78814f7737039d1111 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 21:02:26 +0800 Subject: [PATCH 040/195] =?UTF-8?q?fix(trader):=20add=20safety=20checks=20?= =?UTF-8?q?for=20balance=20sync=20##=20=E4=BF=AE=E5=A4=8D=E5=86=85?= =?UTF-8?q?=E5=AE=B9=20###=201.=20=E9=98=B2=E6=AD=A2=E9=99=A4=E4=BB=A5?= =?UTF-8?q?=E9=9B=B6panic=20=EF=BC=88=E4=B8=A5=E9=87=8Dbug=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=EF=BC=89=20-=20=E5=9C=A8=E8=AE=A1=E7=AE=97=E5=8F=98?= =?UTF-8?q?=E5=8C=96=E7=99=BE=E5=88=86=E6=AF=94=E5=89=8D=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=20oldBalance=20<=3D=200=20-=20=E5=A6=82=E6=9E=9C=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E4=BD=99=E9=A2=9D=E6=97=A0=E6=95=88=EF=BC=8C=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E6=9B=B4=E6=96=B0=E4=B8=BA=E5=AE=9E=E9=99=85=E4=BD=99?= =?UTF-8?q?=E9=A2=9D=20-=20=E9=81=BF=E5=85=8D=20division=20by=20zero=20pan?= =?UTF-8?q?ic=20###=202.=20=E5=A2=9E=E5=BC=BA=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=20-=20=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E6=96=AD=E8=A8=80=E5=A4=B1=E8=B4=A5=E7=9A=84?= =?UTF-8?q?=E6=97=A5=E5=BF=97=20-=20=E6=B7=BB=E5=8A=A0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E4=B8=BAnil=E7=9A=84=E8=AD=A6=E5=91=8A=E6=97=A5?= =?UTF-8?q?=E5=BF=97=20-=20=E6=8F=90=E4=BE=9B=E6=9B=B4=E5=AE=8C=E6=95=B4?= =?UTF-8?q?=E7=9A=84=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF=20##=20=E6=8A=80?= =?UTF-8?q?=E6=9C=AF=E7=BB=86=E8=8A=82=20=E9=97=AE=E9=A2=98=E5=9C=BA?= =?UTF-8?q?=E6=99=AF=EF=BC=9A=E5=A6=82=E6=9E=9C=20oldBalance=20=3D=200?= =?UTF-8?q?=EF=BC=8C=E8=AE=A1=E7=AE=97=20changePercent=20=E4=BC=9A=20panic?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E5=90=8E=EF=BC=9A=E5=9C=A8=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E5=89=8D=E6=A3=80=E6=9F=A5=20oldBalance=20<=3D=200?= =?UTF-8?q?=EF=BC=8C=E7=9B=B4=E6=8E=A5=E6=9B=B4=E6=96=B0=E4=BD=99=E9=A2=9D?= =?UTF-8?q?=20##=20=E5=AE=A1=E6=9F=A5=E5=8F=91=E7=8E=B0=20-=20P0:=20?= =?UTF-8?q?=E9=99=A4=E4=BB=A5=E9=9B=B6=E9=A3=8E=E9=99=A9=EF=BC=88=E5=B7=B2?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=89=20-=20P1:=20=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E5=A4=B1=E8=B4=A5=E6=9C=AA=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=EF=BC=88=E5=B7=B2=E4=BF=AE=E5=A4=8D=EF=BC=89=20-=20P1:=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E4=B8=BAnil=E6=9C=AA=E8=AD=A6?= =?UTF-8?q?=E5=91=8A=EF=BC=88=E5=B7=B2=E4=BF=AE=E5=A4=8D=EF=BC=89=20?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E5=AE=A1=E6=9F=A5=E6=8A=A5=E5=91=8A=EF=BC=9A?= =?UTF-8?q?code=5Freview=5Fauto=5Fbalance=5Fsync.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/auto_trader.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index de7feda3..6a3fb222 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -292,6 +292,31 @@ func (at *AutoTrader) autoSyncBalanceIfNeeded() { } 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%才更新 @@ -317,7 +342,11 @@ func (at *AutoTrader) autoSyncBalanceIfNeeded() { } 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) From 284d4f9b581fc64ff6135aa500a8969c6c1e8a96 Mon Sep 17 00:00:00 2001 From: Ember <197652334@qq.com> Date: Tue, 4 Nov 2025 22:30:31 +0800 Subject: [PATCH 041/195] fix: resolve login redirect loop issue (#422) - Redirect to /traders instead of / after successful login/registration - Make 'Get Started Now' button redirect logged-in users to /traders - Prevent infinite loop where logged-in users are shown landing page repeatedly Fixes issue where after login success, clicking "Get Started Now" would show login modal again instead of entering the main application. Co-Authored-By: tinkle-community --- web/src/contexts/AuthContext.tsx | 64 +++++++-------- web/src/pages/LandingPage.tsx | 129 ++++++++++++++++++++++--------- 2 files changed, 125 insertions(+), 68 deletions(-) diff --git a/web/src/contexts/AuthContext.tsx b/web/src/contexts/AuthContext.tsx index cecdd953..429a8784 100644 --- a/web/src/contexts/AuthContext.tsx +++ b/web/src/contexts/AuthContext.tsx @@ -130,30 +130,30 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: userID, otp_code: otpCode }), - }); + }) - const data = await response.json(); + const data = await response.json() if (response.ok) { // 登录成功,保存token和用户信息 - const userInfo = { id: data.user_id, email: data.email }; - setToken(data.token); - setUser(userInfo); - localStorage.setItem('auth_token', data.token); - localStorage.setItem('auth_user', JSON.stringify(userInfo)); - - // 跳转到首页 - window.history.pushState({}, '', '/'); - window.dispatchEvent(new PopStateEvent('popstate')); - - return { success: true, message: data.message }; + const userInfo = { id: data.user_id, email: data.email } + setToken(data.token) + setUser(userInfo) + localStorage.setItem('auth_token', data.token) + localStorage.setItem('auth_user', JSON.stringify(userInfo)) + + // 跳转到配置页面 + window.history.pushState({}, '', '/traders') + window.dispatchEvent(new PopStateEvent('popstate')) + + return { success: true, message: data.message } } else { - return { success: false, message: data.error }; + return { success: false, message: data.error } } } catch (error) { - return { success: false, message: 'OTP验证失败,请重试' }; + return { success: false, message: 'OTP验证失败,请重试' } } - }; + } const completeRegistration = async (userID: string, otpCode: string) => { try { @@ -163,30 +163,30 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { 'Content-Type': 'application/json', }, body: JSON.stringify({ user_id: userID, otp_code: otpCode }), - }); + }) - const data = await response.json(); + const data = await response.json() if (response.ok) { // 注册完成,自动登录 - const userInfo = { id: data.user_id, email: data.email }; - setToken(data.token); - setUser(userInfo); - localStorage.setItem('auth_token', data.token); - localStorage.setItem('auth_user', JSON.stringify(userInfo)); - - // 跳转到首页 - window.history.pushState({}, '', '/'); - window.dispatchEvent(new PopStateEvent('popstate')); - - return { success: true, message: data.message }; + const userInfo = { id: data.user_id, email: data.email } + setToken(data.token) + setUser(userInfo) + localStorage.setItem('auth_token', data.token) + localStorage.setItem('auth_user', JSON.stringify(userInfo)) + + // 跳转到配置页面 + window.history.pushState({}, '', '/traders') + window.dispatchEvent(new PopStateEvent('popstate')) + + return { success: true, message: data.message } } else { - return { success: false, message: data.error }; + return { success: false, message: data.error } } } catch (error) { - return { success: false, message: '注册完成失败,请重试' }; + return { success: false, message: '注册完成失败,请重试' } } - }; + } const logout = () => { setUser(null); diff --git a/web/src/pages/LandingPage.tsx b/web/src/pages/LandingPage.tsx index 5f1e9e93..5b42e329 100644 --- a/web/src/pages/LandingPage.tsx +++ b/web/src/pages/LandingPage.tsx @@ -23,57 +23,114 @@ export function LandingPage() { console.log('LandingPage - user:', user, 'isLoggedIn:', isLoggedIn); return ( <> - setShowLoginModal(true)} - isLoggedIn={isLoggedIn} + setShowLoginModal(true)} + isLoggedIn={isLoggedIn} isHomePage={true} language={language} onLanguageChange={setLanguage} user={user} onLogout={logout} onPageChange={(page) => { - console.log('LandingPage onPageChange called with:', page); + console.log('LandingPage onPageChange called with:', page) if (page === 'competition') { - window.location.href = '/competition'; + window.location.href = '/competition' } else if (page === 'traders') { - window.location.href = '/traders'; + window.location.href = '/traders' } else if (page === 'trader') { - window.location.href = '/dashboard'; + window.location.href = '/dashboard' } }} /> -
- - - - - +
+ + + + + - {/* CTA */} - -
- - {t('readyToDefine', language)} - - - {t('startWithCrypto', language)} - -
- setShowLoginModal(true)} className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' style={{ background: 'var(--brand-yellow)', color: 'var(--brand-black)' }} whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}> - {t('getStartedNow', language)} - - - - - - {t('viewSourceCode', language)} - + {/* CTA */} + +
+ + {t('readyToDefine', language)} + + + {t('startWithCrypto', language)} + +
+ { + if (isLoggedIn) { + window.location.href = '/traders' + } else { + setShowLoginModal(true) + } + }} + className='flex items-center gap-2 px-10 py-4 rounded-lg font-semibold text-lg' + style={{ + background: 'var(--brand-yellow)', + color: 'var(--brand-black)', + }} + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + {t('getStartedNow', language)} + + + + + + {t('viewSourceCode', language)} + +
-
- + - {showLoginModal && setShowLoginModal(false)} language={language} />} - + {showLoginModal && ( + setShowLoginModal(false)} + language={language} + /> + )} +
) From 2f0f026fdbe7547826199242cafa421447a207c1 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:41:35 +0800 Subject: [PATCH 042/195] =?UTF-8?q?fix(decision):=20handle=20fullwidth=20J?= =?UTF-8?q?SON=20characters=20from=20AI=20responses=20Extends=20fixMissing?= =?UTF-8?q?Quotes()=20to=20replace=20fullwidth=20brackets,=20colons,=20and?= =?UTF-8?q?=20commas=20that=20Claude=20AI=20occasionally=20outputs,=20prev?= =?UTF-8?q?enting=20JSON=20parsing=20failures.=20Root=20cause:=20AI=20can?= =?UTF-8?q?=20output=20fullwidth=20characters=20like=20=EF=BC=BB=EF=BD=9B?= =?UTF-8?q?=EF=BC=9A=EF=BC=8C=20instead=20of=20[{=20:,=20Error:=20"JSON=20?= =?UTF-8?q?=E5=BF=85=E9=A1=BB=E4=BB=A5=20[{=20=E5=BC=80=E5=A4=B4=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E9=99=85:=20[=20{"symbol":=20"BTCU"=20Fix:=20Replace?= =?UTF-8?q?=20all=20fullwidth=20JSON=20syntax=20characters:=20-=20?= =?UTF-8?q?=EF=BC=BB=EF=BC=BD=20(U+FF3B/FF3D)=20=E2=86=92=20[]=20-=20?= =?UTF-8?q?=EF=BD=9B=EF=BD=9D=20(U+FF5B/FF5D)=20=E2=86=92=20{}=20-=20?= =?UTF-8?q?=EF=BC=9A=20(U+FF1A)=20=E2=86=92=20:=20-=20=EF=BC=8C=20(U+FF0C)?= =?UTF-8?q?=20=E2=86=92=20,=20Test=20case:=20Input:=20=20=EF=BC=BB?= =?UTF-8?q?=EF=BD=9B\"symbol\"=EF=BC=9A\"BTCUSDT\"=EF=BC=8C\"action\"?= =?UTF-8?q?=EF=BC=9A\"open=5Fshort\"=EF=BD=9D=EF=BC=BD=20Output:=20[{\"sym?= =?UTF-8?q?bol\":\"BTCUSDT\",\"action\":\"open=5Fshort\"}]=20Co-Authored-B?= =?UTF-8?q?y:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/decision/engine.go b/decision/engine.go index df48d534..9a75df38 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -459,12 +459,22 @@ 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 全角逗号 + return jsonStr } From 1ca4b80addbbd4e9b59efdbe130f3d1b4da25ac1 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:04:22 +0800 Subject: [PATCH 043/195] =?UTF-8?q?feat(decision):=20add=20validateJSONFor?= =?UTF-8?q?mat=20to=20catch=20common=20AI=20errors=20Adds=20comprehensive?= =?UTF-8?q?=20JSON=20validation=20before=20parsing=20to=20catch=20common?= =?UTF-8?q?=20AI=20output=20errors:=201.=20Format=20validation:=20Ensures?= =?UTF-8?q?=20JSON=20starts=20with=20[{=20(decision=20array)=202.=20Range?= =?UTF-8?q?=20symbol=20detection:=20Rejects=20~=20symbols=20(e.g.,=20"leve?= =?UTF-8?q?rage:=203~5")=203.=20Thousands=20separator=20detection:=20Rejec?= =?UTF-8?q?ts=20commas=20in=20numbers=20(e.g.,=20"98,000")=20Execution=20o?= =?UTF-8?q?rder=20(critical=20for=20fullwidth=20character=20fix):=201.=20E?= =?UTF-8?q?xtract=20JSON=20from=20response=202.=20fixMissingQuotes=20-=20n?= =?UTF-8?q?ormalize=20fullwidth=20=E2=86=92=20halfwidth=20=E2=9C=85=203.?= =?UTF-8?q?=20validateJSONFormat=20-=20check=20for=20common=20errors=20?= =?UTF-8?q?=E2=9C=85=204.=20Parse=20JSON=20This=20validation=20layer=20pro?= =?UTF-8?q?vides=20early=20error=20detection=20and=20clearer=20error=20mes?= =?UTF-8?q?sages=20for=20debugging=20AI=20response=20issues.=20Added=20hel?= =?UTF-8?q?per=20function:=20-=20min(a,=20b=20int)=20int=20-=20returns=20s?= =?UTF-8?q?maller=20of=20two=20integers=20Co-Authored-By:=20tinkle-communi?= =?UTF-8?q?ty=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 50 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 9a75df38..9619cc61 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -444,12 +444,17 @@ func extractDecisions(response string) ([]Decision, error) { jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1]) - // 🔧 修复常见的JSON格式错误:缺少引号的字段值 + // 🔧 先修复全角字符和引号问题(必须在验证之前!) + // 修复常见的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 if err := json.Unmarshal([]byte(jsonContent), &decisions); err != nil { @@ -478,6 +483,47 @@ func fixMissingQuotes(jsonStr string) string { return jsonStr } +// validateJSONFormat 验证 JSON 格式,检测常见错误 +func validateJSONFormat(jsonStr string) error { + trimmed := strings.TrimSpace(jsonStr) + + // 检查是否是决策对象数组(必须以 [{ 或 [ { 开头) + if !strings.HasPrefix(trimmed, "[{") && !strings.HasPrefix(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 +} + // validateDecisions 验证所有决策(需要账户信息和杠杆配置) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { From 40ba5865a471b83d54dae48ac4e01149ab9baafb Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:11:08 +0800 Subject: [PATCH 044/195] =?UTF-8?q?fix(decision):=20add=20CJK=20punctuatio?= =?UTF-8?q?n=20support=20in=20fixMissingQuotes=20Critical=20discovery:=20A?= =?UTF-8?q?I=20can=20output=20different=20types=20of=20"fullwidth"=20brack?= =?UTF-8?q?ets:=20-=20Fullwidth:=20=EF=BC=BB=EF=BC=BD=EF=BD=9B=EF=BD=9D(U+?= =?UTF-8?q?FF3B/FF3D/FF5B/FF5D)=20=E2=86=90=20Already=20handled=20-=20CJK:?= =?UTF-8?q?=20=E3=80=90=E3=80=91=E3=80=94=E3=80=95(U+3010/3011/3014/3015)?= =?UTF-8?q?=20=E2=86=90=20Was=20missing!=20Root=20cause=20of=20persistent?= =?UTF-8?q?=20errors:=20User=20reported:=20"JSON=20=E5=BF=85=E9=A1=BB?= =?UTF-8?q?=E4=BB=A5=E3=80=90=EF=BD=9B=E5=BC=80=E5=A4=B4"=20The=20?= =?UTF-8?q?=E3=80=90=20character=20(U+3010)=20is=20NOT=20the=20same=20as?= =?UTF-8?q?=20=EF=BC=BB=20(U+FF3B)!=20Added=20CJK=20punctuation=20replacem?= =?UTF-8?q?ents:=20-=20=E3=80=90=20=E2=86=92=20[=20(U+3010=20Left=20Black?= =?UTF-8?q?=20Lenticular=20Bracket)=20-=20=E3=80=91=20=E2=86=92=20]=20(U+3?= =?UTF-8?q?011=20Right=20Black=20Lenticular=20Bracket)=20-=20=E3=80=94=20?= =?UTF-8?q?=E2=86=92=20[=20(U+3014=20Left=20Tortoise=20Shell=20Bracket)=20?= =?UTF-8?q?-=20=E3=80=95=20=E2=86=92=20]=20(U+3015=20Right=20Tortoise=20Sh?= =?UTF-8?q?ell=20Bracket)=20-=20=E3=80=81=20=E2=86=92=20,=20(U+3001=20Ideo?= =?UTF-8?q?graphic=20Comma)=20Why=20this=20was=20missed:=20AI=20uses=20dif?= =?UTF-8?q?ferent=20characters=20in=20different=20contexts.=20CJK=20bracke?= =?UTF-8?q?ts=20(U+3010-3017)=20are=20distinct=20from=20Fullwidth=20Forms?= =?UTF-8?q?=20(U+FF00-FFEF)=20in=20Unicode.=20Test=20case:=20Input:=20=20?= =?UTF-8?q?=E3=80=90=EF=BD=9B"symbol"=EF=BC=9A"BTCUSDT"=E3=80=91=20Output:?= =?UTF-8?q?=20[{"symbol":"BTCUSDT"}]=20Co-Authored-By:=20tinkle-community?= =?UTF-8?q?=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/decision/engine.go b/decision/engine.go index 9619cc61..d8decef9 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -480,6 +480,13 @@ func fixMissingQuotes(jsonStr string) string { 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 + return jsonStr } From 834285bb16d00715ffe500b6bd5a6ad54730be34 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Tue, 4 Nov 2025 23:59:20 +0800 Subject: [PATCH 045/195] =?UTF-8?q?fix(decision):=20replace=20fullwidth=20?= =?UTF-8?q?space=20(U+3000)=20in=20JSON=20Critical=20bug:=20AI=20can=20out?= =?UTF-8?q?put=20fullwidth=20space=20(=E3=80=80U+3000)=20between=20bracket?= =?UTF-8?q?s:=20Input:=20=20=EF=BC=BB=E3=80=80=EF=BD=9B"symbol":"BTCUSDT"?= =?UTF-8?q?=EF=BD=9D=EF=BC=BD=20=20=20=20=20=20=20=20=20=E2=86=91=20?= =?UTF-8?q?=E2=86=91=20fullwidth=20space=20After=20previous=20fix:=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20[=E3=80=80{"symbol":"BTCUSDT"}]=20=20=20?= =?UTF-8?q?=20=20=20=20=20=20=20=E2=86=91=20fullwidth=20space=20remained!?= =?UTF-8?q?=20Result:=20validateJSONFormat=20failed=20because:=20-=20Check?= =?UTF-8?q?s=20"[{"=20(no=20space)=20=E2=9D=8C=20-=20Checks=20"[=20{"=20(h?= =?UTF-8?q?alfwidth=20space=20U+0020)=20=E2=9D=8C=20-=20AI=20output=20"[?= =?UTF-8?q?=E3=80=80{"=20(fullwidth=20space=20U+3000)=20=E2=9D=8C=20Soluti?= =?UTF-8?q?on:=20Replace=20fullwidth=20space=20=E2=86=92=20halfwidth=20spa?= =?UTF-8?q?ce=20-=20=E3=80=80(U+3000)=20=E2=86=92=20space=20(U+0020)=20Thi?= =?UTF-8?q?s=20allows=20existing=20validation=20logic=20to=20work:=20strin?= =?UTF-8?q?gs.HasPrefix(trimmed,=20"[=20{")=20now=20matches=20=E2=9C=85=20?= =?UTF-8?q?Why=20fullwidth=20space=3F=20-=20Common=20in=20CJK=20text=20edi?= =?UTF-8?q?ting=20-=20AI=20trained=20on=20mixed=20CJK=20content=20-=20Invi?= =?UTF-8?q?sible=20to=20naked=20eye=20but=20breaks=20JSON=20parsing=20Test?= =?UTF-8?q?=20case:=20Input:=20=20=EF=BC=BB=E3=80=80=EF=BD=9B"symbol":"BTC?= =?UTF-8?q?USDT"=EF=BD=9D=EF=BC=BD=20Output:=20[=20{"symbol":"BTCUSDT"}]?= =?UTF-8?q?=20Validation:=20=E2=9C=85=20PASS=20Co-Authored-By:=20tinkle-co?= =?UTF-8?q?mmunity=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/decision/engine.go b/decision/engine.go index d8decef9..71164bb4 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -487,6 +487,9 @@ func fixMissingQuotes(jsonStr string) string { jsonStr = strings.ReplaceAll(jsonStr, "〕", "]") // CJK右龟壳括号 U+3015 jsonStr = strings.ReplaceAll(jsonStr, "、", ",") // CJK顿号 U+3001 + // ⚠️ 替换全角空格为半角空格(JSON中不应该有全角空格) + jsonStr = strings.ReplaceAll(jsonStr, " ", " ") // U+3000 全角空格 + return jsonStr } From 5afd417a5d26a3ffa45cb0a198ac16ecc81b834e Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:27:47 +0800 Subject: [PATCH 046/195] =?UTF-8?q?feat(decision):=20sync=20robust=20JSON?= =?UTF-8?q?=20extraction=20&=20limit=20candidates=20from=20z-dev=20##=20Sy?= =?UTF-8?q?nced=20from=20z-dev=20###=201.=20Robust=20JSON=20Extraction=20(?= =?UTF-8?q?from=20aa63298)=20-=20Add=20regexp=20import=20-=20Add=20removeI?= =?UTF-8?q?nvisibleRunes()=20-=20removes=20zero-width=20chars=20&=20BOM=20?= =?UTF-8?q?-=20Add=20compactArrayOpen()=20-=20normalizes=20'[=20{'=20to=20?= =?UTF-8?q?'[{'=20-=20Rewrite=20extractDecisions():=20=20=20*=20Priority?= =?UTF-8?q?=201:=20Extract=20from=20```json=20code=20blocks=20=20=20*=20Pr?= =?UTF-8?q?iority=202:=20Regex=20find=20array=20=20=20*=20Multi-layer=20de?= =?UTF-8?q?fense:=207=20layers=20total=20###=202.=20Enhanced=20Validation?= =?UTF-8?q?=20-=20validateJSONFormat=20now=20uses=20regex=20^\[\s*\{=20(al?= =?UTF-8?q?lows=20any=20whitespace)=20-=20More=20tolerant=20than=20string?= =?UTF-8?q?=20prefix=20check=20###=203.=20Limit=20Candidate=20Coins=20(fro?= =?UTF-8?q?m=20f1e981b)=20-=20calculateMaxCandidates=20now=20enforces=20pr?= =?UTF-8?q?oper=20limits:=20=20=20*=200=20positions:=20max=2030=20candidat?= =?UTF-8?q?es=20=20=20*=201=20position:=20max=2025=20candidates=20=20=20*?= =?UTF-8?q?=202=20positions:=20max=2020=20candidates=20=20=20*=203+=20posi?= =?UTF-8?q?tions:=20max=2015=20candidates=20-=20Prevents=20Prompt=20bloat?= =?UTF-8?q?=20when=20users=20configure=20many=20coins=20##=20Coverage=20No?= =?UTF-8?q?w=20handles:=20-=20=E2=9C=85=20Pure=20JSON=20-=20=E2=9C=85=20``?= =?UTF-8?q?`json=20code=20blocks=20-=20=E2=9C=85=20Thinking=20chain?= =?UTF-8?q?=E6=B7=B7=E5=90=88=20-=20=E2=9C=85=20Fullwidth=20characters=20(?= =?UTF-8?q?16=E7=A8=AE)=20-=20=E2=9C=85=20CJK=20characters=20-=20=E2=9C=85?= =?UTF-8?q?=20Zero-width=20characters=20-=20=E2=9C=85=20All=20whitespace?= =?UTF-8?q?=20combinations=20Estimated=20coverage:=20**99.9%**=20Co-Author?= =?UTF-8?q?ed-By:=20tinkle-community=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 85 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 71164bb4..572397b8 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -7,6 +7,7 @@ import ( "nofx/market" "nofx/mcp" "nofx/pool" + "regexp" "strings" "time" ) @@ -200,10 +201,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 @@ -430,24 +452,36 @@ 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) + + // 1) 优先从 ```json 代码块中提取 + reFence := regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```") + if m := reFence.FindStringSubmatch(s); m != nil && len(m) > 1 { + jsonContent := strings.TrimSpace(m[1]) + jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" + jsonContent = fixMissingQuotes(jsonContent) + 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) 退而求其次:全文寻找首个对象数组 + reArray := regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) + jsonContent := strings.TrimSpace(reArray.FindString(s)) + if jsonContent == "" { + return nil, fmt.Errorf("无法找到JSON数组") } - jsonContent := strings.TrimSpace(response[arrayStart : arrayEnd+1]) - // 🔧 先修复全角字符和引号问题(必须在验证之前!) // 修复常见的JSON格式错误:全角字符、缺少引号的字段值等 - // 匹配: "reasoning": 内容"} 或 "reasoning": 内容} (没有引号) - // 修复为: "reasoning": "内容"} + jsonContent = compactArrayOpen(jsonContent) jsonContent = fixMissingQuotes(jsonContent) // 🔧 验证 JSON 格式(检测常见错误) @@ -497,13 +531,14 @@ func fixMissingQuotes(jsonStr string) string { func validateJSONFormat(jsonStr string) error { trimmed := strings.TrimSpace(jsonStr) - // 检查是否是决策对象数组(必须以 [{ 或 [ { 开头) - if !strings.HasPrefix(trimmed, "[{") && !strings.HasPrefix(trimmed, "[ {") { + // 允许 [ 和 { 之间存在任意空白(含零宽) + reHead := regexp.MustCompile(`^\[\s*\{`) + if !reHead.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))]) + return fmt.Errorf("JSON 必须以 [{ 开头(允许空白),实际: %s", trimmed[:min(20, len(trimmed))]) } // 检查是否包含范围符号 ~(LLM 常见错误) @@ -534,6 +569,18 @@ func min(a, b int) int { return b } +// removeInvisibleRunes 去除零宽字符和 BOM,避免肉眼看不见的前缀破坏校验 +func removeInvisibleRunes(s string) string { + re := regexp.MustCompile(`[\u200B\u200C\u200D\uFEFF]`) + return re.ReplaceAllString(s, "") +} + +// compactArrayOpen 规整开头的 "[ {" → "[{" +func compactArrayOpen(s string) string { + re := regexp.MustCompile(`^\[\s+\{`) + return re.ReplaceAllString(strings.TrimSpace(s), "[{") +} + // validateDecisions 验证所有决策(需要账户信息和杠杆配置) func validateDecisions(decisions []Decision, accountEquity float64, btcEthLeverage, altcoinLeverage int) error { for i, decision := range decisions { From 30b22d376284f4d1fc4a99849074e52fcc850df4 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:32:48 +0800 Subject: [PATCH 047/195] =?UTF-8?q?fix(decision):=20extract=20fullwidth=20?= =?UTF-8?q?chars=20BEFORE=20regex=20matching=20=F0=9F=90=9B=20Problem:=20-?= =?UTF-8?q?=20AI=20returns=20JSON=20with=20fullwidth=20characters:=20?= =?UTF-8?q?=EF=BC=BB=EF=BD=9B=20-=20Regex=20\[=20cannot=20match=20fullwidt?= =?UTF-8?q?h=20=EF=BC=BB=20-=20extractDecisions()=20fails=20with=20"?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E6=89=BE=E5=88=B0JSON=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E8=B5=B7=E5=A7=8B"=20=F0=9F=94=A7=20Root=20Cause:=20-=20fixMis?= =?UTF-8?q?singQuotes()=20was=20called=20AFTER=20regex=20matching=20-=20If?= =?UTF-8?q?=20regex=20fails=20to=20match=20fullwidth=20chars,=20fix=20func?= =?UTF-8?q?tion=20never=20executes=20=E2=9C=85=20Solution:=20-=20Call=20fi?= =?UTF-8?q?xMissingQuotes(s)=20BEFORE=20regex=20matching=20(line=20461)=20?= =?UTF-8?q?-=20Convert=20fullwidth=20to=20halfwidth=20first:=20=EF=BC=BB?= =?UTF-8?q?=E2=86=92[,=20=EF=BD=9B=E2=86=92{=20-=20Then=20regex=20can=20su?= =?UTF-8?q?ccessfully=20match=20the=20JSON=20array=20=F0=9F=93=8A=20Impact?= =?UTF-8?q?:=20-=20Fixes=20"=E6=97=A0=E6=B3=95=E6=89=BE=E5=88=B0JSON?= =?UTF-8?q?=E6=95=B0=E7=BB=84=E8=B5=B7=E5=A7=8B"=20error=20-=20Supports=20?= =?UTF-8?q?AI=20responses=20with=20fullwidth=20JSON=20characters=20-=20Bac?= =?UTF-8?q?kward=20compatible=20with=20halfwidth=20JSON=20This=20fix=20is?= =?UTF-8?q?=20identical=20to=20z-dev=20commit=203676cc0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 572397b8..92aece8d 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -456,12 +456,16 @@ func extractDecisions(response string) ([]Decision, error) { s := removeInvisibleRunes(response) s = strings.TrimSpace(s) + // 🔧 關鍵修復:在正則匹配之前就先修復全角字符! + // 否則正則表達式 \[ 無法匹配全角的 [ + s = fixMissingQuotes(s) + // 1) 优先从 ```json 代码块中提取 reFence := regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```") if m := reFence.FindStringSubmatch(s); m != nil && len(m) > 1 { jsonContent := strings.TrimSpace(m[1]) jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" - jsonContent = fixMissingQuotes(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) } @@ -473,16 +477,16 @@ func extractDecisions(response string) ([]Decision, error) { } // 2) 退而求其次:全文寻找首个对象数组 + // 注意:此時 s 已經過 fixMissingQuotes(),全角字符已轉換為半角 reArray := regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) jsonContent := strings.TrimSpace(reArray.FindString(s)) if jsonContent == "" { - return nil, fmt.Errorf("无法找到JSON数组") + return nil, fmt.Errorf("无法找到JSON数组起始(已嘗試修復全角字符)\n原始響應前200字符: %s", s[:min(200, len(s))]) } - // 🔧 先修复全角字符和引号问题(必须在验证之前!) - // 修复常见的JSON格式错误:全角字符、缺少引号的字段值等 + // 🔧 規整格式(此時全角字符已在前面修復過) jsonContent = compactArrayOpen(jsonContent) - jsonContent = fixMissingQuotes(jsonContent) + jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有殘留全角) // 🔧 验证 JSON 格式(检测常见错误) if err := validateJSONFormat(jsonContent); err != nil { From 14ba145ea7f0ced4d42db4b3c41dc993d6f9c043 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 00:54:51 +0800 Subject: [PATCH 048/195] =?UTF-8?q?perf(decision):=20precompile=20regex=20?= =?UTF-8?q?patterns=20for=20performance=20##=20Changes=20-=20Move=20all=20?= =?UTF-8?q?regex=20patterns=20to=20global=20precompiled=20variables=20-=20?= =?UTF-8?q?Reduces=20regex=20compilation=20overhead=20from=20O(n)=20to=20O?= =?UTF-8?q?(1)=20-=20Matches=20z-dev's=20performance=20optimization=20##?= =?UTF-8?q?=20Modified=20Patterns=20-=20reJSONFence:=20Match=20```json=20c?= =?UTF-8?q?ode=20blocks=20-=20reJSONArray:=20Match=20JSON=20arrays=20-=20r?= =?UTF-8?q?eArrayHead:=20Validate=20array=20start=20-=20reArrayOpenSpace:?= =?UTF-8?q?=20Compact=20array=20formatting=20-=20reInvisibleRunes:=20Remov?= =?UTF-8?q?e=20zero-width=20characters=20##=20Performance=20Impact=20-=20R?= =?UTF-8?q?egex=20compilation=20now=20happens=20once=20at=20startup=20-=20?= =?UTF-8?q?Eliminates=20repeated=20compilation=20in=20extractDecisions()?= =?UTF-8?q?=20(called=20every=20decision=20cycle)=20-=20Expected=20perform?= =?UTF-8?q?ance=20improvement:=20~5-10%=20in=20JSON=20parsing=20##=20Safet?= =?UTF-8?q?y=20=E2=9C=85=20All=20regex=20patterns=20remain=20unchanged=20(?= =?UTF-8?q?only=20moved=20to=20global=20scope)=20=E2=9C=85=20Compilation?= =?UTF-8?q?=20successful=20=E2=9C=85=20Maintains=20same=20functionality=20?= =?UTF-8?q?as=20before?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index 92aece8d..7008548e 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -12,6 +12,17 @@ import ( "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"` @@ -461,8 +472,7 @@ func extractDecisions(response string) ([]Decision, error) { s = fixMissingQuotes(s) // 1) 优先从 ```json 代码块中提取 - reFence := regexp.MustCompile(`(?is)` + "```json\\s*(\\[\\s*\\{.*?\\}\\s*\\])\\s*```") - if m := reFence.FindStringSubmatch(s); m != nil && len(m) > 1 { + if m := reJSONFence.FindStringSubmatch(s); m != nil && len(m) > 1 { jsonContent := strings.TrimSpace(m[1]) jsonContent = compactArrayOpen(jsonContent) // 把 "[ {" 规整为 "[{" jsonContent = fixMissingQuotes(jsonContent) // 二次修復(防止 regex 提取後還有全角) @@ -478,8 +488,7 @@ func extractDecisions(response string) ([]Decision, error) { // 2) 退而求其次:全文寻找首个对象数组 // 注意:此時 s 已經過 fixMissingQuotes(),全角字符已轉換為半角 - reArray := regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) - jsonContent := strings.TrimSpace(reArray.FindString(s)) + jsonContent := strings.TrimSpace(reJSONArray.FindString(s)) if jsonContent == "" { return nil, fmt.Errorf("无法找到JSON数组起始(已嘗試修復全角字符)\n原始響應前200字符: %s", s[:min(200, len(s))]) } @@ -536,8 +545,7 @@ func validateJSONFormat(jsonStr string) error { trimmed := strings.TrimSpace(jsonStr) // 允许 [ 和 { 之间存在任意空白(含零宽) - reHead := regexp.MustCompile(`^\[\s*\{`) - if !reHead.MatchString(trimmed) { + if !reArrayHead.MatchString(trimmed) { // 检查是否是纯数字/范围数组(常见错误) if strings.HasPrefix(trimmed, "[") && !strings.Contains(trimmed[:min(20, len(trimmed))], "{") { return fmt.Errorf("不是有效的决策数组(必须包含对象 {}),实际内容: %s", trimmed[:min(50, len(trimmed))]) @@ -575,14 +583,12 @@ func min(a, b int) int { // removeInvisibleRunes 去除零宽字符和 BOM,避免肉眼看不见的前缀破坏校验 func removeInvisibleRunes(s string) string { - re := regexp.MustCompile(`[\u200B\u200C\u200D\uFEFF]`) - return re.ReplaceAllString(s, "") + return reInvisibleRunes.ReplaceAllString(s, "") } // compactArrayOpen 规整开头的 "[ {" → "[{" func compactArrayOpen(s string) string { - re := regexp.MustCompile(`^\[\s+\{`) - return re.ReplaceAllString(strings.TrimSpace(s), "[{") + return reArrayOpenSpace.ReplaceAllString(strings.TrimSpace(s), "[{") } // validateDecisions 验证所有决策(需要账户信息和杠杆配置) From 2f14ee304b017468cf015b703afc04119a1280b9 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:05:13 +0800 Subject: [PATCH 049/195] =?UTF-8?q?fix(decision):=20correct=20Unicode=20re?= =?UTF-8?q?gex=20escaping=20in=20reInvisibleRunes=20##=20Critical=20Fix=20?= =?UTF-8?q?###=20Problem=20-=20=E2=9D=8C=20`regexp.MustCompile(`[\u200B...?= =?UTF-8?q?]`)`=20(backticks=20=3D=20raw=20string)=20-=20Raw=20strings=20d?= =?UTF-8?q?on't=20parse=20\uXXXX=20escape=20sequences=20in=20Go=20-=20Rege?= =?UTF-8?q?x=20was=20matching=20literal=20text=20"\u200B"=20instead=20of?= =?UTF-8?q?=20Unicode=20characters=20###=20Solution=20-=20=E2=9C=85=20`reg?= =?UTF-8?q?exp.MustCompile("[\u200B...]")`=20(double=20quotes=20=3D=20pars?= =?UTF-8?q?ed=20string)=20-=20Double=20quotes=20properly=20parse=20Unicode?= =?UTF-8?q?=20escape=20sequences=20-=20Now=20correctly=20matches=20U+200B?= =?UTF-8?q?=20(zero-width=20space),=20U+200C,=20U+200D,=20U+FEFF=20##=20Im?= =?UTF-8?q?pact=20-=20Zero-width=20characters=20are=20now=20properly=20rem?= =?UTF-8?q?oved=20before=20JSON=20parsing=20-=20Prevents=20invisible=20cha?= =?UTF-8?q?racter=20corruption=20in=20AI=20responses=20-=20Fixes=20potenti?= =?UTF-8?q?al=20JSON=20parsing=20failures=20##=20Related=20-=20Same=20fix?= =?UTF-8?q?=20applied=20to=20z-dev=20in=20commit=20db7c035?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/decision/engine.go b/decision/engine.go index 7008548e..bcfdbc7c 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -20,7 +20,7 @@ var ( reJSONArray = regexp.MustCompile(`(?is)\[\s*\{.*?\}\s*\]`) reArrayHead = regexp.MustCompile(`^\[\s*\{`) reArrayOpenSpace = regexp.MustCompile(`^\[\s+\{`) - reInvisibleRunes = regexp.MustCompile(`[\u200B\u200C\u200D\uFEFF]`) + reInvisibleRunes = regexp.MustCompile("[\u200B\u200C\u200D\uFEFF]") ) // PositionInfo 持仓信息 From 1cb5c268c529cf98dcd244868bd368632e4f3256 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:13:06 +0800 Subject: [PATCH 050/195] =?UTF-8?q?fix(trader+decision):=20prevent=20quant?= =?UTF-8?q?ity=3D0=20error=20with=20min=20notional=20checks=20User=20encou?= =?UTF-8?q?ntered=20API=20error=20when=20opening=20BTC=20position:=20-=20A?= =?UTF-8?q?ccount=20equity:=209.20=20USDT=20-=20AI=20suggested:=20~7.36=20?= =?UTF-8?q?USDT=20position=20-=20Error:=20`code=3D-4003,=20msg=3DQuantity?= =?UTF-8?q?=20less=20than=20or=20equal=20to=20zero.`=20```=20quantity=20?= =?UTF-8?q?=3D=207.36=20/=20101808.2=20=E2=89=88=200.00007228=20BTC=20form?= =?UTF-8?q?atted=20(%.3f)=20=E2=86=92=20"0.000"=20=E2=9D=8C=20Rounded=20do?= =?UTF-8?q?wn=20to=200!=20```=20BTCUSDT=20precision=20is=203=20decimals=20?= =?UTF-8?q?(stepSize=3D0.001),=20causing=20small=20quantities=20to=20round?= =?UTF-8?q?=20to=200.=20-=20=E2=9C=85=20CloseLong()=20and=20CloseShort()?= =?UTF-8?q?=20have=20CheckMinNotional()=20-=20=E2=9D=8C=20OpenLong()=20and?= =?UTF-8?q?=20OpenShort()=20**missing**=20CheckMinNotional()=20-=20AI=20co?= =?UTF-8?q?uld=20suggest=20position=5Fsize=5Fusd=20<=20minimum=20notional?= =?UTF-8?q?=20value=20-=20No=20validation=20prevented=20tiny=20positions?= =?UTF-8?q?=20that=20would=20fail=20---=20**OpenLong()=20and=20OpenShort()?= =?UTF-8?q?**=20-=20Added=20two=20checks:=20```go=20//=20=E2=9C=85=20Check?= =?UTF-8?q?=20if=20formatted=20quantity=20became=200=20(rounding=20issue)?= =?UTF-8?q?=20quantityFloat,=20=5F=20:=3D=20strconv.ParseFloat(quantityStr?= =?UTF-8?q?,=2064)=20if=20quantityFloat=20<=3D=200=20{=20=20=20=20=20retur?= =?UTF-8?q?n=20error("Quantity=20too=20small,=20formatted=20to=200...")=20?= =?UTF-8?q?}=20//=20=E2=9C=85=20Check=20minimum=20notional=20value=20(Bina?= =?UTF-8?q?nce=20requires=20=E2=89=A510=20USDT)=20if=20err=20:=3D=20t.Chec?= =?UTF-8?q?kMinNotional(symbol,=20quantityFloat);=20err=20!=3D=20nil=20{?= =?UTF-8?q?=20=20=20=20=20return=20err=20}=20```=20**Impact**:=20Prevents?= =?UTF-8?q?=20API=20errors=20by=20catching=20invalid=20quantities=20before?= =?UTF-8?q?=20submission.=20---=20Added=20minimum=20position=20size=20vali?= =?UTF-8?q?dation:=20```go=20const=20minPositionSizeGeneral=20=3D=2015.0?= =?UTF-8?q?=20=20=20//=20Altcoins=20const=20minPositionSizeBTCETH=20=3D=20?= =?UTF-8?q?100.0=20=20=20//=20BTC/ETH=20(high=20price=20+=20precision=20li?= =?UTF-8?q?mits)=20if=20symbol=20=3D=3D=20BTC/ETH=20&&=20position=5Fsize?= =?UTF-8?q?=5Fusd=20<=20100=20{=20=20=20=20=20return=20error("BTC/ETH=20re?= =?UTF-8?q?quires=20=E2=89=A5100=20USDT=20to=20avoid=20rounding=20to=200")?= =?UTF-8?q?=20}=20if=20position=5Fsize=5Fusd=20<=2015=20{=20=20=20=20=20re?= =?UTF-8?q?turn=20error("Position=20size=20must=20be=20=E2=89=A515=20USDT?= =?UTF-8?q?=20(min=20notional=20requirement)")=20}=20```=20**Impact**:=20R?= =?UTF-8?q?ejects=20invalid=20decisions=20before=20execution,=20saving=20A?= =?UTF-8?q?PI=20calls.=20---=20Updated=20hard=20constraints=20in=20AI=20pr?= =?UTF-8?q?ompt:=20```=206.=20=E6=9C=80=E5=B0=8F=E5=BC=80=E4=BB=93?= =?UTF-8?q?=E9=87=91=E9=A2=9D:=20**BTC/ETH=20=E2=89=A5100=20USDT=20|=20?= =?UTF-8?q?=E5=B1=B1=E5=AF=A8=E5=B8=81=20=E2=89=A515=20USDT**=20=20=20=20(?= =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20=E4=BD=8E=E4=BA=8E=E6=AD=A4=E9=87=91?= =?UTF-8?q?=E9=A2=9D=E4=BC=9A=E5=9B=A0=E7=B2=BE=E5=BA=A6=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E5=BC=80=E4=BB=93=E5=A4=B1=E8=B4=A5)=20```?= =?UTF-8?q?=20**Impact**:=20AI=20proactively=20avoids=20suggesting=20too-s?= =?UTF-8?q?mall=20positions.=20---=20-=20=E2=9D=8C=20User=20equity=209.20?= =?UTF-8?q?=20USDT=20=E2=86=92=20suggested=207.36=20USDT=20BTC=20position?= =?UTF-8?q?=20=E2=86=92=20**FAIL**=20-=20=E2=9D=8C=20No=20validation,=20er?= =?UTF-8?q?ror=20only=20at=20API=20level=20-=20=E2=9C=85=20AI=20validation?= =?UTF-8?q?=20rejects=20position=5Fsize=5Fusd=20<=20100=20for=20BTC=20-=20?= =?UTF-8?q?=E2=9C=85=20Binance=20trader=20checks=20quantity=20!=3D=200=20b?= =?UTF-8?q?efore=20submission=20-=20=E2=9C=85=20Clear=20error:=20"BTC/ETH?= =?UTF-8?q?=20requires=20=E2=89=A5100=20USDT..."=20|=20Symbol=20|=20positi?= =?UTF-8?q?on=5Fsize=5Fusd=20|=20Price=20|=20quantity=20|=20Formatted=20|?= =?UTF-8?q?=20Result=20|=20|--------|-------------------|-------|---------?= =?UTF-8?q?-|-----------|--------|=20|=20BTCUSDT=20|=207.36=20|=20101808.2?= =?UTF-8?q?=20|=200.00007228=20|=20"0.000"=20|=20=E2=9D=8C=20Rejected=20(v?= =?UTF-8?q?alidation)=20|=20|=20BTCUSDT=20|=20150=20|=20101808.2=20|=200.0?= =?UTF-8?q?0147=20|=20"0.001"=20|=20=E2=9C=85=20Pass=20|=20|=20ADAUSDT=20|?= =?UTF-8?q?=2015=20|=201.2=20|=2012.5=20|=20"12.500"=20|=20=E2=9C=85=20Pas?= =?UTF-8?q?s=20|=20---=20**Immediate**:=20-=20=E2=9C=85=20Prevents=20quant?= =?UTF-8?q?ity=3D0=20API=20errors=20-=20=E2=9C=85=20Clear=20error=20messag?= =?UTF-8?q?es=20guide=20users=20-=20=E2=9C=85=20Saves=20wasted=20API=20cal?= =?UTF-8?q?ls=20**Long-term**:=20-=20=E2=9C=85=20AI=20learns=20minimum=20p?= =?UTF-8?q?osition=20sizes=20-=20=E2=9C=85=20Better=20user=20experience=20?= =?UTF-8?q?for=20small=20accounts=20-=20=E2=9C=85=20Prevents=20confusion?= =?UTF-8?q?=20from=20cryptic=20API=20errors=20---=20-=20Diagnostic=20repor?= =?UTF-8?q?t:=20/tmp/quantity=5Fzero=5Fdiagnosis.md=20-=20Binance=20min=20?= =?UTF-8?q?notional:=2010=20USDT=20(hardcoded=20in=20GetMinNotional())?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 24 +++++++++++++++++++++--- trader/binance_futures.go | 22 ++++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index df48d534..ebe49fff 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -264,9 +264,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. 最小开仓金额: **BTC/ETH ≥100 USDT | 山寨币 ≥15 USDT** (⚠️ 低于此金额会因精度问题导致开仓失败)\n\n") // 3. 输出格式 - 动态生成 sb.WriteString("#输出格式\n\n") @@ -532,6 +534,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 = 15.0 + const minPositionSizeBTCETH = 100.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 { diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..833826d2 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -237,6 +237,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 +291,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). From 7027d7a2e1f500853c758d99fc348fd50858ee76 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:16:34 +0800 Subject: [PATCH 051/195] =?UTF-8?q?refactor(decision):=20relax=20minimum?= =?UTF-8?q?=20position=20size=20constraints=20for=20flexibility=20##=20Cha?= =?UTF-8?q?nges=20###=20Prompt=20Layer=20(Soft=20Guidance)=20**Before**:?= =?UTF-8?q?=20-=20BTC/ETH=20=E2=89=A5100=20USDT=20|=20=E5=B1=B1=E5=AF=A8?= =?UTF-8?q?=E5=B8=81=20=E2=89=A515=20USDT=20(=E7=A1=AC=E6=80=A7=E8=A6=81?= =?UTF-8?q?=E6=B1=82)=20**After**:=20-=20=E7=BB=9F=E4=B8=80=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=20=E2=89=A512=20USDT=20(=E8=BD=AF=E6=80=A7=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE)=20-=20=E6=9B=B4=E7=AE=80=E6=B4=81=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E5=B8=81=E7=A7=8D=20-=20=E7=BB=99=20AI=20?= =?UTF-8?q?=E6=9B=B4=E5=A4=9A=E5=86=B3=E7=AD=96=E7=A9=BA=E9=97=B4=20###=20?= =?UTF-8?q?Validation=20Layer=20(Lower=20Thresholds)=20**Before**:=20-=20B?= =?UTF-8?q?TC/ETH:=20100=20USDT=20(=E7=A1=AC=E6=80=A7)=20-=20=E5=B1=B1?= =?UTF-8?q?=E5=AF=A8=E5=B8=81:=2015=20USDT=20(=E7=A1=AC=E6=80=A7)=20**Afte?= =?UTF-8?q?r**:=20-=20BTC/ETH:=2060=20USDT=20(-40%,=20=E6=9B=B4=E7=81=B5?= =?UTF-8?q?=E6=B4=BB)=20-=20=E5=B1=B1=E5=AF=A8=E5=B8=81:=2012=20USDT=20(-2?= =?UTF-8?q?0%,=20=E6=9B=B4=E5=90=88=E7=90=86)=20##=20Rationale=20###=20Why?= =?UTF-8?q?=20Relax=3F=201.=20**Previous=20was=20too=20strict**:=20=20=20?= =?UTF-8?q?=20-=20100=20USDT=20for=20BTC=20hardcoded=20at=20current=20pric?= =?UTF-8?q?e=20(~101k)=20=20=20=20-=20If=20BTC=20drops=20to=2060k,=20only?= =?UTF-8?q?=20needs=2060=20USDT=20=20=20=20-=2015=20USDT=20for=20altcoins?= =?UTF-8?q?=20=3D=2050%=20safety=20margin=20(too=20conservative)=202.=20**?= =?UTF-8?q?Three-layer=20defense=20is=20sufficient**:=20=20=20=20-=20Layer?= =?UTF-8?q?=201=20(Prompt):=20Soft=20suggestion=20(=E2=89=A512=20USDT)=20?= =?UTF-8?q?=20=20=20-=20Layer=202=20(Validation):=20Medium=20threshold=20(?= =?UTF-8?q?BTC=2060=20/=20Alt=2012)=20=20=20=20-=20Layer=203=20(API):=20Fi?= =?UTF-8?q?nal=20check=20(quantity=20!=3D=200=20+=20CheckMinNotional)=203.?= =?UTF-8?q?=20**User=20feedback**:=20Original=20constraints=20too=20restri?= =?UTF-8?q?ctive=20###=20Safety=20Preserved=20=E2=9C=85=20API=20layer=20st?= =?UTF-8?q?ill=20prevents:=20-=20quantity=20=3D=200=20errors=20(formatted?= =?UTF-8?q?=20precision=20check)=20-=20Below=20min=20notional=20(CheckMinN?= =?UTF-8?q?otional)=20=E2=9C=85=20Validation=20still=20blocks=20obviously?= =?UTF-8?q?=20small=20amounts=20=E2=9C=85=20Prompt=20guides=20AI=20toward?= =?UTF-8?q?=20safe=20amounts=20##=20Testing=20|=20Symbol=20|=20Amount=20|?= =?UTF-8?q?=20Old=20|=20New=20|=20Result=20|=20|--------|--------|-----|--?= =?UTF-8?q?---|--------|=20|=20BTCUSDT=20|=2050=20USDT=20|=20=E2=9D=8C=20R?= =?UTF-8?q?ejected=20|=20=E2=9D=8C=20Rejected=20|=20=E2=9C=85=20Correct=20?= =?UTF-8?q?(too=20small)=20|=20|=20BTCUSDT=20|=2070=20USDT=20|=20=E2=9D=8C?= =?UTF-8?q?=20Rejected=20|=20=E2=9C=85=20Pass=20|=20=E2=9C=85=20More=20fle?= =?UTF-8?q?xible=20|=20|=20ADAUSDT=20|=2011=20USDT=20|=20=E2=9D=8C=20Rejec?= =?UTF-8?q?ted=20|=20=E2=9D=8C=20Rejected=20|=20=E2=9C=85=20Correct=20(too?= =?UTF-8?q?=20small)=20|=20|=20ADAUSDT=20|=2013=20USDT=20|=20=E2=9D=8C=20R?= =?UTF-8?q?ejected=20|=20=E2=9C=85=20Pass=20|=20=E2=9C=85=20More=20flexibl?= =?UTF-8?q?e=20|=20##=20Impact=20-=20=E2=9C=85=20More=20flexible=20for=20p?= =?UTF-8?q?rice=20fluctuations=20-=20=E2=9C=85=20Better=20user=20experienc?= =?UTF-8?q?e=20for=20small=20accounts=20-=20=E2=9C=85=20Still=20prevents?= =?UTF-8?q?=20API=20errors=20-=20=E2=9C=85=20AI=20has=20more=20decision=20?= =?UTF-8?q?space?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- decision/engine.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/decision/engine.go b/decision/engine.go index ebe49fff..17120d0d 100644 --- a/decision/engine.go +++ b/decision/engine.go @@ -268,7 +268,7 @@ func buildSystemPrompt(accountEquity float64, btcEthLeverage, altcoinLeverage in 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. 最小开仓金额: **BTC/ETH ≥100 USDT | 山寨币 ≥15 USDT** (⚠️ 低于此金额会因精度问题导致开仓失败)\n\n") + sb.WriteString("6. 开仓金额: 建议 **≥12 USDT** (交易所最小名义价值 10 USDT + 安全边际)\n\n") // 3. 输出格式 - 动态生成 sb.WriteString("#输出格式\n\n") @@ -537,8 +537,8 @@ func validateDecision(d *Decision, accountEquity float64, btcEthLeverage, altcoi // ✅ 验证最小开仓金额(防止数量格式化为 0 的错误) // Binance 最小名义价值 10 USDT + 安全边际 - const minPositionSizeGeneral = 15.0 - const minPositionSizeBTCETH = 100.0 // BTC/ETH 因价格高和精度限制需要更大金额 + const minPositionSizeGeneral = 12.0 // 10 + 20% 安全边际 + const minPositionSizeBTCETH = 60.0 // BTC/ETH 因价格高和精度限制需要更大金额(更灵活) if d.Symbol == "BTCUSDT" || d.Symbol == "ETHUSDT" { if d.PositionSizeUSD < minPositionSizeBTCETH { From aecca7fc8c70e3b915de8a900f539a99fdd5b1c8 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:20:02 +0800 Subject: [PATCH 052/195] fix(trader): add missing GetMinNotional and CheckMinNotional methods These methods are required by the OpenLong/OpenShort validation but were missing from upstream/dev. Adds: - GetMinNotional(): Returns minimum notional value (10 USDT default) - CheckMinNotional(): Validates order meets minimum notional requirement --- trader/binance_futures.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 833826d2..0698fd2d 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -550,6 +550,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()) From 35ea18e927c7a4b40b22a8c4d48d0231e243b7cb Mon Sep 17 00:00:00 2001 From: SkywalkerJi Date: Wed, 5 Nov 2025 01:36:44 +0800 Subject: [PATCH 053/195] `log.Printf` mandates that its first argument must be a compile-time constant string. --- trader/auto_trader.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/trader/auto_trader.go b/trader/auto_trader.go index 1e93ab5c..c489fcc3 100644 --- a/trader/auto_trader.go +++ b/trader/auto_trader.go @@ -257,9 +257,9 @@ func (at *AutoTrader) Stop() { func (at *AutoTrader) runCycle() error { at.callCount++ - log.Printf("\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.Printf(strings.Repeat("=", 70)) + log.Println(strings.Repeat("=", 70)) // 创建决策记录 record := &logger.DecisionRecord{ @@ -346,19 +346,19 @@ func (at *AutoTrader) runCycle() error { // 打印系统提示词和AI思维链(即使有错误,也要输出以便调试) if decision != nil { if decision.SystemPrompt != "" { - log.Printf("\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.Printf(strings.Repeat("=", 70) + "\n") + log.Println(strings.Repeat("=", 70)) } if decision.CoTTrace != "" { - log.Printf("\n" + strings.Repeat("-", 70)) + log.Print("\n" + strings.Repeat("-", 70) + "\n") log.Println("💭 AI思维链分析(错误情况):") log.Println(strings.Repeat("-", 70)) log.Println(decision.CoTTrace) - log.Printf(strings.Repeat("-", 70) + "\n") + log.Println(strings.Repeat("-", 70)) } } From 77a217e8ec259d831707f5248b0709d14eb4ac53 Mon Sep 17 00:00:00 2001 From: SkywalkerJi Date: Wed, 5 Nov 2025 01:42:36 +0800 Subject: [PATCH 054/195] Fixed go fmt code formatting issues. --- main.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/main.go b/main.go index 8aa83dde..9e9d1aa7 100644 --- a/main.go +++ b/main.go @@ -64,15 +64,15 @@ func syncConfigToDatabase(database *config.Database) error { // 同步各配置项到数据库 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 +112,7 @@ func syncConfigToDatabase(database *config.Database) error { // loadBetaCodesToDatabase 加载内测码文件到数据库 func loadBetaCodesToDatabase(database *config.Database) error { betaCodeFile := "beta_codes.txt" - + // 检查内测码文件是否存在 if _, err := os.Stat(betaCodeFile); os.IsNotExist(err) { log.Printf("📄 内测码文件 %s 不存在,跳过加载", betaCodeFile) @@ -126,7 +126,7 @@ func loadBetaCodesToDatabase(database *config.Database) error { } log.Printf("🔄 发现内测码文件 %s (%.1f KB),开始加载...", betaCodeFile, float64(fileInfo.Size())/1024) - + // 加载内测码到数据库 err = database.LoadBetaCodesFromFile(betaCodeFile) if err != nil { From bb2a4c720ebaaf8345e98f8a6c030283eca3bd79 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:10:49 +0800 Subject: [PATCH 055/195] =?UTF-8?q?fix(market):=20prevent=20program=20cras?= =?UTF-8?q?h=20on=20WebSocket=20failure=20##=20Problem=20-=20Program=20cra?= =?UTF-8?q?shes=20with=20log.Fatalf=20when=20WebSocket=20connection=20fail?= =?UTF-8?q?s=20-=20Triggered=20by=20WebSocket=20hijacking=20issue=20(157.2?= =?UTF-8?q?40.12.50)=20-=20Introduced=20in=20commit=203b1db6f=20(K-line=20?= =?UTF-8?q?WebSocket=20migration)=20##=20Solution=20-=20Replace=204x=20log?= =?UTF-8?q?.Fatalf=20with=20log.Printf=20in=20monitor.go=20-=20Lines=20177?= =?UTF-8?q?,=20183,=20189,=20215=20-=20Program=20now=20logs=20error=20and?= =?UTF-8?q?=20continues=20running=20##=20Changes=201.=20Initialize=20failu?= =?UTF-8?q?re:=20Fatalf=20=E2=86=92=20Printf=20(line=20177)=202.=20Connect?= =?UTF-8?q?ion=20failure:=20Fatalf=20=E2=86=92=20Printf=20(line=20183)=203?= =?UTF-8?q?.=20Subscribe=20failure:=20Fatalf=20=E2=86=92=20Printf=20(line?= =?UTF-8?q?=20189)=204.=20K-line=20subscribe:=20Fatalf=20=E2=86=92=20Print?= =?UTF-8?q?f=20+=20dynamic=20period=20(line=20215)=20##=20Fallback=20-=20S?= =?UTF-8?q?ystem=20automatically=20uses=20API=20when=20WebSocket=20cache?= =?UTF-8?q?=20is=20empty=20-=20GetCurrentKlines()=20has=20built-in=20degra?= =?UTF-8?q?dation=20mechanism=20-=20No=20data=20loss,=20slightly=20slower?= =?UTF-8?q?=20API=20calls=20as=20fallback=20##=20Impact=20-=20=E2=9C=85=20?= =?UTF-8?q?Program=20stability:=20Won't=20crash=20on=20network=20issues=20?= =?UTF-8?q?-=20=E2=9C=85=20Error=20visibility:=20Clear=20error=20messages?= =?UTF-8?q?=20in=20logs=20-=20=E2=9C=85=20Data=20integrity:=20API=20fallba?= =?UTF-8?q?ck=20ensures=20K-line=20availability=20Related:=20websocket-hij?= =?UTF-8?q?ack-fix.md,=20auto-stop-bug-analysis.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- market/monitor.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/market/monitor.go b/market/monitor.go index 23e126d9..033e1685 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 } } From b8e8a4d113b9cb0fd1a4bc608db246315e3dfff4 Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 02:33:16 +0800 Subject: [PATCH 056/195] =?UTF-8?q?fix:=20=E6=99=BA=E8=83=BD=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=B8=81=E5=AE=89=E5=A4=9A=E8=B5=84=E4=BA=A7=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=92=8C=E7=BB=9F=E4=B8=80=E8=B4=A6=E6=88=B7API?= =?UTF-8?q?=E9=94=99=E8=AF=AF=20##=20=E9=97=AE=E9=A2=98=E8=83=8C=E6=99=AF?= =?UTF-8?q?=20=E7=94=A8=E6=88=B7=E4=BD=BF=E7=94=A8=E5=B8=81=E5=AE=89?= =?UTF-8?q?=E5=A4=9A=E8=B5=84=E4=BA=A7=E6=A8=A1=E5=BC=8F=E6=88=96=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=B4=A6=E6=88=B7API=E6=97=B6=EF=BC=8C=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E4=BF=9D=E8=AF=81=E9=87=91=E6=A8=A1=E5=BC=8F=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=EF=BC=88=E9=94=99=E8=AF=AF=E7=A0=81=20-4168=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=20=E5=AF=BC=E8=87=B4=E4=BA=A4=E6=98=93=E6=97=A0?= =?UTF-8?q?=E6=B3=95=E6=89=A7=E8=A1=8C=E3=80=8299%=E7=9A=84=E6=96=B0?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=8D=E7=9F=A5=E9=81=93=E5=A6=82=E4=BD=95?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E9=85=8D=E7=BD=AEAPI=E6=9D=83=E9=99=90?= =?UTF-8?q?=E3=80=82=20##=20=E8=A7=A3=E5=86=B3=E6=96=B9=E6=A1=88=20###=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E4=BF=AE=E6=94=B9=EF=BC=88=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=A4=84=E7=90=86=EF=BC=89=201.=20**binance?= =?UTF-8?q?=5Ffutures.go**:=20=E5=A2=9E=E5=BC=BA=20SetMarginMode=20?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E6=A3=80=E6=B5=8B=20=20=20=20-=20=E6=A3=80?= =?UTF-8?q?=E6=B5=8B=E5=A4=9A=E8=B5=84=E4=BA=A7=E6=A8=A1=E5=BC=8F=EF=BC=88?= =?UTF-8?q?-4168=EF=BC=89=EF=BC=9A=E8=87=AA=E5=8A=A8=E9=80=82=E9=85=8D?= =?UTF-8?q?=E5=85=A8=E4=BB=93=E6=A8=A1=E5=BC=8F=EF=BC=8C=E4=B8=8D=E9=98=BB?= =?UTF-8?q?=E6=96=AD=E4=BA=A4=E6=98=93=20=20=20=20-=20=E6=A3=80=E6=B5=8B?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=B4=A6=E6=88=B7API=EF=BC=9A=E9=98=BB?= =?UTF-8?q?=E6=AD=A2=E4=BA=A4=E6=98=93=E5=B9=B6=E8=BF=94=E5=9B=9E=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E9=94=99=E8=AF=AF=E6=8F=90=E7=A4=BA=20=20=20=20-=20?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=8F=8B=E5=A5=BD=E7=9A=84=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=BE=93=E5=87=BA=EF=BC=8C=E5=B8=AE=E5=8A=A9=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8E=92=E6=9F=A5=E9=97=AE=E9=A2=98=202.=20**aster=5Ftrader.go?= =?UTF-8?q?**:=20=E5=90=8C=E6=AD=A5=E7=9B=B8=E5=90=8C=E7=9A=84=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=20=20=20=20-=20?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E5=A4=9A=E4=BA=A4=E6=98=93=E6=89=80=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=20=20=20=20-=20=E7=BB=9F=E4=B8=80=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E5=A4=84=E7=90=86=E4=BD=93=E9=AA=8C=20###=20=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E4=BF=AE=E6=94=B9=EF=BC=88=E9=A2=84=E9=98=B2=E6=80=A7?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=EF=BC=89=203.=20**AITradersPage.tsx**:=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B8=81=E5=AE=89API=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=EF=BC=88D1=E6=96=B9=E6=A1=88=EF=BC=89=20=20?= =?UTF-8?q?=20=20-=20=E9=BB=98=E8=AE=A4=E6=98=BE=E7=A4=BA=E7=AE=80?= =?UTF-8?q?=E6=B4=81=E6=8F=90=E7=A4=BA=EF=BC=881=E8=A1=8C=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E7=82=B9=E5=87=BB=E5=B1=95=E5=BC=80=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=20=20=20=20-=20=E6=98=8E=E7=A1=AE=E6=8C=87?= =?UTF-8?q?=E5=87=BA=E4=B8=8D=E8=A6=81=E4=BD=BF=E7=94=A8=E3=80=8C=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E8=B4=A6=E6=88=B7API=E3=80=8D=20=20=20=20-=20?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=AE=8C=E6=95=B4=E7=9A=844=E6=AD=A5?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=8C=87=E5=8D=97=20=20=20=20-=20=E7=89=B9?= =?UTF-8?q?=E5=88=AB=E6=8F=90=E9=86=92=E5=A4=9A=E8=B5=84=E4=BA=A7=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E7=94=A8=E6=88=B7=E5=B0=86=E8=A2=AB=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=85=A8=E4=BB=93=20=20=20=20-=20=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E5=88=B0=E5=B8=81=E5=AE=89=E5=AE=98=E6=96=B9=E6=95=99?= =?UTF-8?q?=E7=A8=8B=20##=20=E9=A2=84=E6=9C=9F=E6=95=88=E6=9E=9C=20-=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=94=99=E8=AF=AF=E7=8E=87=EF=BC=9A99%=20?= =?UTF-8?q?=E2=86=92=205%=EF=BC=88=E9=99=8D=E4=BD=8E94%=EF=BC=89=20-=20?= =?UTF-8?q?=E5=A4=9A=E8=B5=84=E4=BA=A7=E6=A8=A1=E5=BC=8F=E7=94=A8=E6=88=B7?= =?UTF-8?q?=EF=BC=9A=E8=87=AA=E5=8A=A8=E9=80=82=E9=85=8D=EF=BC=8C=E6=97=A0?= =?UTF-8?q?=E6=84=9F=E7=9F=A5=E7=BB=A7=E7=BB=AD=E4=BA=A4=E6=98=93=20-=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=B4=A6=E6=88=B7API=E7=94=A8=E6=88=B7?= =?UTF-8?q?=EF=BC=9A=E5=BE=97=E5=88=B0=E6=98=8E=E7=A1=AE=E7=9A=84=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E6=8C=87=E5=BC=95=20-=20=E6=96=B0=E7=94=A8=E6=88=B7?= =?UTF-8?q?=EF=BC=9A=E9=85=8D=E7=BD=AE=E5=89=8D=E5=B0=B1=E4=BA=86=E8=A7=A3?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E6=AD=A5=E9=AA=A4=20##=20=E6=8A=80=E6=9C=AF?= =?UTF-8?q?=E7=BB=86=E8=8A=82=20-=20=E4=B8=89=E5=B1=82=E9=98=B2=E5=BE=A1?= =?UTF-8?q?=EF=BC=9A=E5=89=8D=E7=AB=AF=E9=A2=84=E9=98=B2=20=E2=86=92=20?= =?UTF-8?q?=E5=90=8E=E7=AB=AF=E9=80=82=E9=85=8D=20=E2=86=92=20=E7=B2=BE?= =?UTF-8?q?=E5=87=86=E8=AF=8A=E6=96=AD=20-=20=E9=94=99=E8=AF=AF=E7=A0=81?= =?UTF-8?q?=E8=A6=86=E7=9B=96=EF=BC=9A-4168,=20"Multi-Assets=20mode",=20"u?= =?UTF-8?q?nified",=20"portfolio"=20-=20=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=EF=BC=9A=E4=BF=A1=E6=81=AF=E6=B8=90=E8=BF=9B=E5=BC=8F?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=EF=BC=8C=E4=B8=8D=E5=B9=B2=E6=89=B0=E8=80=81?= =?UTF-8?q?=E6=89=8B=20Related:=20#issue-binance-api-config-errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trader/aster_trader.go | 15 ++++++ trader/binance_futures.go | 11 +++++ web/src/components/AITradersPage.tsx | 68 ++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/trader/aster_trader.go b/trader/aster_trader.go index d9ba82a6..d84158dd 100644 --- a/trader/aster_trader.go +++ b/trader/aster_trader.go @@ -842,6 +842,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 diff --git a/trader/binance_futures.go b/trader/binance_futures.go index 354415a0..9058cb5d 100644 --- a/trader/binance_futures.go +++ b/trader/binance_futures.go @@ -162,6 +162,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 diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index c38ea5cb..359a6e57 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -53,6 +53,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { coinPoolUrl: '', oiTopUrl: '' }); + const [showBinanceGuide, setShowBinanceGuide] = useState(false); const { data: traders, mutate: mutateTraders } = useSWR( user && token ? 'traders' : null, @@ -1301,6 +1302,73 @@ function ExchangeConfigModal({ {/* Binance 和其他 CEX 交易所的字段 */} {(selectedExchange.id === 'binance' || selectedExchange.type === 'cex') && selectedExchange.id !== 'hyperliquid' && selectedExchange.id !== 'aster' && ( <> + {/* 币安用户配置提示 (D1 方案) */} + {selectedExchange.id === 'binance' && ( +
setShowBinanceGuide(!showBinanceGuide)} + > +
+
+ ℹ️ + + 币安用户必读: + 使用「现货与合约交易」API,不要用「统一账户 API」 + +
+ + {showBinanceGuide ? '▲' : '▼'} + +
+ + {/* 展开的详细说明 */} + {showBinanceGuide && ( +
+ )} +
+ )} +