Compare commits

..

24 Commits

Author SHA1 Message Date
WrBug 6a6af6f5e9 fix: handle neg_risk market in sellPosition
Fixes #38

The sellPosition method was not checking if the market uses Neg Risk
Exchange contract. For neg_risk markets, the order signing must use the
Neg Risk Exchange address, otherwise the order will be rejected by
Polymarket.

The copy trading service already handles this correctly, but the manual
sell position flow was missing this check.
2026-04-29 14:35:43 +08:00
WrBug 4b58b7cd46 Merge pull request #57 from WrBug/dev 2026-04-28 22:32:45 +08:00
WrBug 8ab8ae5205 style: 全局将 USDC 展示替换为 $ 符号
前端所有金额展示从 "100.00 USDC" 改为 "$100.00" 格式,
后端 Telegram 通知模板和错误提示同步更新。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 22:23:49 +08:00
WrBug 0769022878 feat: 添加 CLOB 2.0 迁移弹窗提醒与账户页迁移引导
登录后首次进入显示迁移提醒弹窗,引导用户前往账户页;
账户页首次进入时显示 Alert 提示 USDC.e → pUSD 迁移按钮;
移动端账户卡片补充 swap 操作按钮。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 21:59:15 +08:00
WrBug 06a122dd34 fix: 迁移链路外统一改为仅识别 pUSD
移除链上监听中对 USDC.e 的抵押品匹配,并将解包与审批相关说明统一为 pUSD 语义,避免在非 swap/wrap 流程继续使用 USDC.e。

Made-with: Cursor
2026-04-28 21:40:41 +08:00
WrBug fb6b469e9f fix: 修复 CLOB V2 下代理交易与链上成交解析问题
调整 CLOB API 地址并优化 Magic Proxy 的执行与 gas 限制,避免 wrap 与 relay 调用在 V2 环境下异常回滚。同时补充 pUSD/USDC.e 兼容与 ERC1155-only 成交价格回填逻辑,提升链上成交解析稳定性。

Made-with: Cursor
2026-04-28 21:34:43 +08:00
WrBug 22a6544373 feat: CLOB V1 → V2 完整迁移
- 更新 EIP-712 签名结构: domain version "1"→"2", 移除 taker/expiration/nonce/feeRateBps, 新增 timestamp/metadata/builder
- 更新合约地址为 V2 (CTF_EXCHANGE, NEG_RISK_EXCHANGE)
- CLOB URL 切换到 clob-v2.polymarket.com
- 抵押品代币 USDC.e → pUSD (0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB)
- 新增 USDC.e → pUSD wrap 功能 (CollateralOnramp 合约)
- SignedOrderObject 补充 API payload 必需的 taker/expiration 字段
- NewOrderRequest 补充 deferExec/postOnly 字段
- 前端账户管理新增 pUSD 迁移按钮
- 移除所有 feeRateBps 相关逻辑

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 22:24:50 +08:00
WrBug 04b7505094 Merge branch 'dev'
Made-with: Cursor
2026-03-30 12:55:20 +08:00
WrBug 2056417749 Merge pull request #41 from WrBug/ai_fix/n_39
fix: backtest equity curve bugs
2026-03-30 11:32:36 +08:00
WrBug 748c871af4 fix: backtest equity curve bugs (#39)
- Fix SELL logic: correctly reduce position.quantity after selling (core bug)
  Previously positions were never decremented, causing sold positions to be
  settled again at backtest end, inflating final balance.
- Fix settleRemainingPositions: profitLoss now correctly computes
  settlementValue - cost (was incorrectly using settlementValue.negate())
- Fix max drawdown calculation: use current balance instead of previous
  iteration's runningBalance
- Frontend: add comment noting chart shows cash balance, not total equity
2026-03-28 18:06:06 +08:00
WrBug 53f1381c3b chore: 移除 Bug 报告模板中手动添加 label 的文案
Made-with: Cursor
2026-03-10 12:53:06 +08:00
WrBug 4a3ebb674e chore: 移除 Bug 报告模板中的 AI 自动修复确认复选框
Made-with: Cursor
2026-03-10 12:46:41 +08:00
WrBug 2b570a432a fix(ci): 解析 Issue 编号时 grep 无匹配导致 step 退出
PR 描述中无 Closes/Fixes/Resolves #N 时,grep 返回 1,set -e 导致脚本
在未执行分支名回退逻辑前即退出。为管道添加 || true,使无匹配时继续
从分支名 ai_fix/N_xxx 解析 Issue 编号。

Made-with: Cursor
2026-03-10 12:44:52 +08:00
WrBug 5412a0eb49 Merge pull request #37 from WrBug/ai_fix/35_bug_1usdc
fix: #35 [Bug] / 价差策略按比例买入,购买的价值小于1usdc时,会下单失败
2026-03-10 12:40:12 +08:00
WrBug 7b4e702da9 ci(close-issue): add Telegram notification when AI-fixed issue is closed
- Get issue details (title, url) for TG message
- Send TG via TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID (same as docker-build)
- Use GH_TOKEN from GITHUB_TOKEN for gh CLI

Made-with: Cursor
2026-03-10 12:39:33 +08:00
WrBug e32697e7ee ci: add workflow to close issue when PR is merged
- Trigger on pull_request closed (merged) to main
- Parse issue number from PR body (Closes #N) or branch name (ai_fix/N_xxx)
- Close linked issue via GitHub API if still open

Made-with: Cursor
2026-03-10 12:34:35 +08:00
WrBug d8a75fc8dd fix: #35 [Bug] / 价差策略按比例买入,购买的价值小于1usdc时,会下单失败 2026-03-10 12:18:03 +08:00
WrBug 6cb48bb6cc Update AI bug report template to bilingual (English/Chinese) 2026-03-10 02:59:34 +08:00
WrBug e78785de7c Add AI bug report template 2026-03-10 02:59:33 +08:00
WrBug 915d4570df Update AI bug report template to bilingual (English/Chinese) 2026-03-10 02:39:50 +08:00
WrBug 48d6e82f43 Add AI bug report template 2026-03-10 02:30:26 +08:00
WrBug 0fb015f6d3 Merge pull request #33 from WrBug/dev
Release v2.3.3: 加密价差策略与尾盘监控优化
2026-02-25 21:34:54 +08:00
WrBug a0c2d7995b Merge pull request #32 from WrBug/dev
Release v2.3.2: 尾盘策略多市场与币安按需订阅
2026-02-20 23:50:10 +08:00
WrBug 7c1f8df590 Merge pull request #31 from WrBug/dev
feat: 账户设置、尾盘策略与文档整理
2026-02-18 03:29:38 +08:00
56 changed files with 2293 additions and 463 deletions
+201
View File
@@ -0,0 +1,201 @@
---
name: 🤖 Bug Report for AI Fix / AI Bug 报告
description: Bug 报告模板(提交后请手动添加 'fix via ai' 标签触发自动修复)
title: '[Bug] / '
assignees: []
body:
- type: markdown
attributes:
value: |
## Bug Report Template 🐛
本模板用于报告 PolyHermes 项目的 bug。
⚠️ **Important / 重要提示**
- After submission, if you need AI auto-fix, please manually add the `fix via ai` label
/ 提交后,如需 AI 自动修复,请手动添加 `fix via ai` 标签
- AI fixes will be on the `fix_issues_by_ai` branch / AI 修复将在 `fix_issues_by_ai` 分支上进行
- All AI fixes require human review before merging / 所有 AI 修复需要人工审核后才能合并
- Security vulnerabilities, database migrations, major changes are not recommended for AI auto-fix
/ 涉及安全漏洞、数据库迁移、重大变更等问题不建议使用 AI 自动修复
---
本模板用于报告 PolyHermes 项目的 bug。
This template is for reporting bugs in the PolyHermes project.
- type: textarea
id: description
attributes:
label: 📝 Bug Description / Bug 描述
description: Clearly and concisely describe the bug / 清晰简洁地描述这个 bug
placeholder: Describe the bug you encountered / 描述你遇到的问题...
validations:
required: true
- type: dropdown
id: type
attributes:
label: 🎯 Bug Type / 问题类型
description: Select the type of bug / 选择问题类型
options:
- Frontend bug (UI/UX/interaction) / 前端 bug (UI/UX/交互问题)
- Backend bug (API/logic/data) / 后端 bug (API/逻辑/数据处理)
- Database issue / 数据库问题
- Performance issue / 性能问题
- Configuration/Deployment / 配置/部署问题
- Documentation / 文档问题
- Other / 其他
validations:
required: true
- type: dropdown
id: scope
attributes:
label: 📍 Affected Scope / 影响范围
description: Select the scope of impact / 选择问题影响范围
options:
- Specific page/function only / 仅影响特定页面/功能
- Entire system / 影响整个系统
- Specific user role / 影响特定用户角色
- Only in specific environment / 仅在特定环境下重现
validations:
required: true
- type: textarea
id: steps
attributes:
label: 🔍 Steps to Reproduce / 复现步骤
description: Provide clear, detailed steps to reproduce the bug / 提供清晰、详细的步骤来重现这个 bug
placeholder: |
1. Visit page: `...` / 访问页面:`...`
2. Click button: `...` / 点击按钮:`...`
3. Input data: `...` / 输入数据:`...`
4. Submit form: `...` / 提交表单:`...`
5. Observe error: `...` / 观察到错误:`...`
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: Reproduction Frequency / 复现频率
options:
- Always reproducible (100%) / 总是能复现 (100%)
- Frequently reproducible (50%+) / 经常能复现 (50%+)
- Occasionally reproducible (<50%) / 偶尔能复现 (<50%)
- Hard to reproduce / 很难复现
validations:
required: true
- type: textarea
id: expected
attributes:
label: 💻 Expected Behavior / 预期行为
description: Describe what you expected to happen / 描述你期望发生什么
placeholder: What should happen / 应该发生什么...
validations:
required: true
- type: textarea
id: actual
attributes:
label: ❌ Actual Behavior / 实际行为
description: Describe what actually happened / 描述实际发生了什么
placeholder: What actually happened / 实际发生了什么...
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: 📸 Screenshots / Recordings / 截图/录屏
description: If applicable, add screenshots or recordings to illustrate the problem (drag and drop files here)
/ 如果适用,添加截图或录屏来说明问题(可以拖拽文件到这里)
placeholder: Add screenshots or recordings / 添加截图或录屏...
- type: textarea
id: environment
attributes:
label: 🌐 Environment / 环境
description: Provide relevant environment information / 提供相关环境信息
value: |
**Browser (for frontend issues) / 浏览器(前端问题):**
- Browser: ______ / 浏览器:______
- Browser version: ______ / 浏览器版本:______
- Operating System: ______ / 操作系统:______
**Backend Environment (for backend issues) / 后端环境(后端问题):**
- Node.js version: ______ / Node.js 版本:______
- Database version: ______ / 数据库版本:______
- Docker version (if used): ______ / Docker 版本(如果使用):______
- Other relevant dependencies: ______ / 其他相关依赖版本:______
validations:
required: false
- type: textarea
id: related-files
attributes:
label: 📁 Related Files / Code / 相关文件/代码
description: Provide relevant file paths or code snippets / 提供可能涉及的文件路径或相关代码片段
placeholder: |
Possibly related files / 可能涉及的文件:
- frontend/src/components/...
- backend/src/main/kotlin/...
Error logs / 错误日志:
```
Paste error logs here / 粘贴错误日志
```
validations:
required: false
- type: textarea
id: suggestions
attributes:
label: 🎯 Fix Suggestions (Optional) / 修复建议(可选)
description: If you have fix ideas, describe them briefly / 如果你有修复思路,可以在这里简单描述
placeholder: |
Suggest adding ZZZ check in the YYY method of file XXX
/ 建议在 XXX 文件的 YYY 方法中,添加 ZZZ 检查
...
- type: dropdown
id: priority
attributes:
label: 🚨 Priority / 优先级
options:
- 🔴 High - Blocking core functionality, affects user experience / 高 - 阻塞核心功能,影响用户体验
- 🟡 Medium - Limited functionality but not blocking / 中 - 功能受限但不阻塞
- 🟢 Low - Minor issue, doesn't affect usage / 低 - 小问题,不影响使用
validations:
required: true
- type: textarea
id: additional
attributes:
label: 📝 Additional Information / 补充说明
description: Any other information that helps AI understand and fix the issue / 任何其他有助于 AI 理解和修复问题的信息
placeholder: |
- Was this bug introduced recently? / 这个 bug 是最近引入的吗?
- Is it related to a specific PR or commit? / 是否与某个特定的 PR 或 commit 相关?
- Does it only occur with specific datasets or users? / 是否只在特定数据集或特定用户情况下出现?
/ ...
Any other context / 其他任何上下文信息...
- type: checkboxes
id: ai-fix-approval
attributes:
label: 🤖 AI Auto-Fix Confirmation / AI 自动修复确认
description: If you need AI to auto-fix this issue, check the options below (remember to add 'fix via ai' label after submission)
/ 如需 AI 自动修复此 issue,请勾选下方选项(提交后记得添加 'fix via ai' 标签)
options:
- label: |
I understand the AI auto-fix workflow and agree to manually review the AI-created PR
/ 我已了解 AI 自动修复的工作流程,并同意在 AI 创建 PR 后进行人工审核
required: false
- label: |
This issue is suitable for AI auto-fix (not a security vulnerability, not a database migration, not a major change)
/ 此问题适合 AI 自动修复(非安全漏洞、非数据库迁移、非重大变更)
required: false
@@ -0,0 +1,127 @@
# PR 合并后自动关闭关联的 Issue
# 当 PR 从 ai_fix/N_xxx 分支合并到 main 时,关闭 #N 对应的 Issue(若 PR 描述中未含 Closes #N 则通过分支名解析)
# 关闭后发送 Telegram 通知(复用 TELEGRAM_BOT_TOKEN / TELEGRAM_CHAT_ID
name: Close issue on PR merge
on:
pull_request:
types: [closed]
branches: [main]
jobs:
close-issue:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- name: Get PR info
id: pr
run: |
# 从 PR body 查找 Closes #N / Fixes #N(若已有则 GitHub 已自动关 issue,本 step 仅做解析)
BODY="${{ github.event.pull_request.body }}"
HEAD_REF="${{ github.event.pull_request.head.ref }}"
# 优先从 PR 描述解析(无匹配时 grep 会 exit 1,需 || true 避免 set -e 导致脚本退出)
ISSUE_NUM=$(echo "$BODY" | grep -oE '(Closes|Fixes|Resolves) #([0-9]+)' | head -1 | grep -oE '[0-9]+' || true)
if [ -z "$ISSUE_NUM" ]; then
# 从分支名解析 ai_fix/N_xxx
ISSUE_NUM=$(echo "$HEAD_REF" | sed -n 's|^ai_fix/\([0-9]*\)_.*|\1|p')
fi
if [ -z "$ISSUE_NUM" ]; then
echo "ISSUE_NUMBER=" >> $GITHUB_OUTPUT
echo "skip=true" >> $GITHUB_OUTPUT
echo "未从 PR 描述或分支名解析到 Issue 编号,跳过关闭"
exit 0
fi
echo "ISSUE_NUMBER=$ISSUE_NUM" >> $GITHUB_OUTPUT
echo "skip=false" >> $GITHUB_OUTPUT
echo "解析到 Issue #$ISSUE_NUM"
- name: Get issue details
if: steps.pr.outputs.skip != 'true'
id: issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ISSUE_NUM="${{ steps.pr.outputs.ISSUE_NUMBER }}"
# 获取 issue 标题与 URL(用于 TG 消息)
JSON=$(gh issue view "$ISSUE_NUM" --json title,url 2>/dev/null || echo '{"title":"","url":""}')
TITLE=$(echo "$JSON" | jq -r '.title')
ISSUE_URL=$(echo "$JSON" | jq -r '.url')
echo "title<<EOF" >> $GITHUB_OUTPUT
echo "$TITLE" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "url=$ISSUE_URL" >> $GITHUB_OUTPUT
- name: Close issue
if: steps.pr.outputs.skip != 'true'
uses: actions/github-script@v7
with:
script: |
const issueNumber = parseInt('${{ steps.pr.outputs.ISSUE_NUMBER }}', 10);
if (!issueNumber || isNaN(issueNumber)) {
console.log('No valid issue number, skip');
return;
}
const { data: issue } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber
});
if (issue.state === 'open') {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: 'closed'
});
console.log(`Issue #${issueNumber} closed.`);
} else {
console.log(`Issue #${issueNumber} already closed.`);
}
- name: Send Telegram notification
if: steps.pr.outputs.skip != 'true'
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
# 与 docker-build 一致:未配置则跳过
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
echo "⚠️ Telegram Bot Token 或 Chat ID 未配置,跳过通知"
exit 0
fi
ISSUE_NUM="${{ steps.pr.outputs.ISSUE_NUMBER }}"
ISSUE_TITLE="${{ steps.issue.outputs.title }}"
ISSUE_URL="${{ steps.issue.outputs.url }}"
PR_URL="${{ github.event.pull_request.html_url }}"
# 与 docker-build 相同的 HTML 消息格式
MESSAGE="✅ <b>AI 修复的 Issue 已关闭</b>"$'\n'$'\n'"🔢 <b>Issue:</b> #${ISSUE_NUM} ${ISSUE_TITLE}"$'\n'"📎 <a href=\"${ISSUE_URL}\">查看 Issue</a>"$'\n'"🔗 <a href=\"${PR_URL}\">查看 PR</a>"
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg chat_id "$TELEGRAM_CHAT_ID" \
--arg text "$MESSAGE" \
'{chat_id: $chat_id, text: $text, parse_mode: "HTML", disable_web_page_preview: false}')" > /tmp/telegram_response.json
if [ $? -eq 0 ]; then
RESPONSE=$(cat /tmp/telegram_response.json)
if echo "$RESPONSE" | grep -q '"ok":true'; then
echo "✅ Telegram 通知发送成功"
else
echo "❌ Telegram 通知发送失败: $RESPONSE"
exit 0
fi
else
echo "❌ 发送 Telegram 消息时发生错误"
exit 0
fi
+2 -1
View File
@@ -112,4 +112,5 @@ __pycache__/
clob-client/
builder-relayer-client/
landing-page/
clob-client-v2/
settings.local.json
File diff suppressed because it is too large Load Diff
@@ -73,7 +73,7 @@ interface PolymarketClobApi {
*/
@POST("/orders/batch")
suspend fun createOrdersBatch(
@Body request: CreateOrdersBatchRequest
@Body request: List<NewOrderRequest>
): Response<List<OrderResponse>>
/**
@@ -174,22 +174,25 @@ interface PolymarketClobApi {
// 请求和响应数据类
/**
* 签名的订单对象(根据官方文档)
* 参考: https://docs.polymarket.com/developers/CLOB/orders/create-order
* V2 签名的订单对象
* EIP-712 签名字段: salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder
* API payload 额外字段: taker, expiration (不在 EIP-712 签名中,但 API 请求需要)
* 参考: clob-client-v2/src/types/ordersV2.ts NewOrderV2
*/
data class SignedOrderObject(
val salt: Long, // random salt used to create unique order
val maker: String, // maker address (funder)
val signer: String, // signing address
val taker: String, // taker address (operator)
val taker: String, // taker address (zero address for public orders, NOT in EIP-712 signing)
val tokenId: String, // ERC1155 token ID of conditional token being traded
val makerAmount: String, // maximum amount maker is willing to spend
val takerAmount: String, // minimum amount taker will pay the maker in return
val expiration: String, // unix expiration timestamp
val nonce: String, // maker's exchange nonce of the order is associated
val feeRateBps: String, // fee rate basis points as required by the operator
val side: String, // buy or sell enum index ("BUY" or "SELL")
val signatureType: Int, // signature type enum index
val timestamp: String, // order creation time in milliseconds (V2)
val expiration: String, // expiration timestamp unix seconds, "0" = no expiration (NOT in EIP-712 signing)
val metadata: String, // bytes32 metadata (V2)
val builder: String, // bytes32 builder code (V2)
val signature: String // hex encoded signature
)
@@ -198,10 +201,11 @@ data class SignedOrderObject(
* 参考: https://docs.polymarket.com/developers/CLOB/orders/create-order
*/
data class NewOrderRequest(
val order: SignedOrderObject, // signed object
val order: SignedOrderObject, // V2 signed object
val owner: String, // api key of order owner
val orderType: String, // order type ("FOK", "GTC", "GTD", "FAK")
val deferExec: Boolean = false // defer execution flag
val deferExec: Boolean = false, // defer execution
val postOnly: Boolean = false // post only (maker-only)
)
/**
@@ -235,26 +239,6 @@ data class NewOrderResponse(
}
}
/**
* 旧的订单请求格式(已废弃,保留用于兼容)
* @deprecated 使用 NewOrderRequest 代替
*/
@Deprecated("使用 NewOrderRequest 代替,需要签名的订单对象")
data class CreateOrderRequest(
val market: String? = null, // condition ID(可选,如果提供tokenId则不需要)
val token_id: String? = null, // token ID(可选,如果提供market则不需要)
val side: String, // "BUY" or "SELL"
val price: String,
val size: String,
val type: String = "LIMIT",
val expiration: Long? = null
)
@Deprecated("使用 NewOrderRequest 代替")
data class CreateOrdersBatchRequest(
val orders: List<NewOrderRequest>
)
data class CancelOrdersBatchRequest(
val orderIds: List<String>
)
@@ -572,5 +572,53 @@ class AccountController(
}
}
/**
* 将 USDC.e wrap 为 pUSDV2 迁移)
*/
@PostMapping("/wrap-to-pusd")
fun wrapToPusd(@RequestBody request: Map<String, Any>): ResponseEntity<ApiResponse<Map<String, String?>>> {
return try {
val accountId = (request["accountId"] as? Number)?.toLong()
?: return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
val result = runBlocking { accountService.wrapUsdcToPusd(accountId) }
result.fold(
onSuccess = { txHash ->
ResponseEntity.ok(ApiResponse.success(mapOf("transactionHash" to txHash)))
},
onFailure = { e ->
logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("USDC.e → pUSD wrap 异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
}
}
/**
* 查询 USDC.e 余额(V2 迁移用)
*/
@PostMapping("/usdce-balance")
fun getUsdceBalance(@RequestBody request: Map<String, Any>): ResponseEntity<ApiResponse<Map<String, String>>> {
return try {
val accountId = (request["accountId"] as? Number)?.toLong()
?: return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource))
val result = runBlocking { accountService.getUsdceBalance(accountId) }
result.fold(
onSuccess = { balance ->
ResponseEntity.ok(ApiResponse.success(mapOf("balance" to balance.toPlainString())))
},
onFailure = { e ->
logger.error("查询 USDC.e 余额失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
}
)
} catch (e: Exception) {
logger.error("查询 USDC.e 余额异常: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, e.message, messageSource))
}
}
}
@@ -267,7 +267,7 @@ class CryptoTailStrategyController(
"当前周期已下单" -> ErrorCode.PARAM_ERROR
"价格必须在 0~1 之间" -> ErrorCode.PARAM_ERROR
"数量不能少于 1" -> ErrorCode.PARAM_ERROR
"总金额不能少于 1 USDC" -> ErrorCode.PARAM_ERROR
"总金额不能少于 $1" -> ErrorCode.PARAM_ERROR
"总金额超过策略配置的投入金额" -> ErrorCode.PARAM_ERROR
else -> ErrorCode.SERVER_ERROR
}
@@ -365,14 +365,14 @@ class AccountService(
}
/**
* Polymarket 代币批准检查:USDC.e 需授权的 spender 合约地址(Polygon 主网)
* Polymarket 代币批准检查:pUSD 需授权的 spender 合约地址(Polygon 主网)
* 来源:Polymarket/magic-safe-builder-example README §6 Token Approvals
* 及 neg-risk-ctf-adapter 仓库 addresses.json (chainId 137)
*/
private val setupApprovalSpenders = mapOf(
"CTF_CONTRACT" to "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045", // Conditional Tokens
"CTF_EXCHANGE" to "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E", // 普通市场交易所
"NEG_RISK_EXCHANGE" to "0xC5d563A36AE78145C45a50134d48A1215220f80a", // 负风险市场交易所
"CTF_EXCHANGE" to "0xE111180000d2663C0091e4f400237545B87B996B", // 普通市场交易所
"NEG_RISK_EXCHANGE" to "0xe2222d279d744050d28e00520010520000310F59", // 负风险市场交易所
"NEG_RISK_ADAPTER" to "0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296" // 负风险适配器(非 WCOL 地址)
)
@@ -941,7 +941,7 @@ class AccountService(
}
/**
* 轮询用:遍历所有账户,对代理地址 WCOL 余额 > 0 的执行解包为 USDC.e
* 轮询用:遍历所有账户,对代理地址 WCOL 余额 > 0 的执行解包。
* 由 WcolUnwrapJobService 每 20 秒调用,赎回后无需在赎回流程内等待确认与解包。
*/
suspend fun runWcolUnwrapForAllAccounts() {
@@ -1244,23 +1244,17 @@ class AccountService(
else -> "GTC"
}
// GTC 和 FOK 订单的 expiration 必须为 "0"
// 只有 GTD 订单才需要设置具体的过期时间
val expiration = "0"
// 7. 解密私钥
val decryptedPrivateKey = decryptPrivateKey(account)
// 获取费率(根据 Polymarket Maker Rebates Program 要求)
val feeRateResult = clobService.getFeeRate(tokenId)
val feeRateBps = if (feeRateResult.isSuccess) {
feeRateResult.getOrNull()?.toString() ?: "0"
} else {
logger.warn("获取费率失败,使用默认值 0: tokenId=$tokenId, error=${feeRateResult.exceptionOrNull()?.message}")
"0"
// 11. 检查市场是否为 Neg Risk 市场,获取正确的 Exchange 合约地址
val negRisk = marketService.getNegRiskByConditionId(request.marketId) == true
val exchangeContract = orderSigningService.getExchangeContract(negRisk)
if (negRisk) {
logger.debug("市场为 Neg Risk,使用 Neg Risk Exchange 签约: conditionId=${request.marketId}")
}
// 11. 创建并签名订单(使用计算后的卖出数量,按账户钱包类型使用对应 signatureType
// 12. 创建并签名订单(使用计算后的卖出数量,按账户钱包类型使用对应 signatureType
val signedOrder = try {
orderSigningService.createAndSignOrder(
privateKey = decryptedPrivateKey,
@@ -1270,22 +1264,19 @@ class AccountService(
price = sellPrice,
size = sellQuantity.toPlainString(), // 使用计算后的卖出数量
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType),
nonce = "0",
feeRateBps = feeRateBps, // 使用动态获取的费率
expiration = expiration
exchangeContract = exchangeContract
)
} catch (e: Exception) {
logger.error("创建并签名订单失败", e)
return Result.failure(Exception("创建并签名订单失败: ${e.message}"))
}
// 12. 构建订单请求
// 13. 构建订单请求
val newOrderRequest = com.wrbug.polymarketbot.api.NewOrderRequest(
order = signedOrder,
owner = account.apiKey, // API Key
orderType = orderType,
deferExec = false
orderType = orderType
)
// 13. 解密 API 凭证并使用账户的API凭证创建订单
@@ -1925,6 +1916,32 @@ class AccountService(
false
}
}
/**
* 将账户的 USDC.e wrap 为 pUSD
*/
suspend fun wrapUsdcToPusd(accountId: Long): Result<String?> {
val account = accountRepository.findById(accountId).orElse(null)
?: return Result.failure(IllegalArgumentException("账户不存在"))
if (account.proxyAddress.isBlank()) {
return Result.failure(IllegalStateException("账户代理地址不存在"))
}
val privateKey = cryptoUtils.decrypt(account.privateKey)
val walletType = WalletType.fromStringOrDefault(account.walletType, WalletType.SAFE)
return blockchainService.wrapUsdcToPusd(privateKey, account.proxyAddress, walletType)
}
/**
* 查询 USDC.e 余额(用于迁移提示)
*/
suspend fun getUsdceBalance(accountId: Long): Result<BigDecimal> {
val account = accountRepository.findById(accountId).orElse(null)
?: return Result.failure(IllegalArgumentException("账户不存在"))
if (account.proxyAddress.isBlank()) {
return Result.failure(IllegalStateException("账户代理地址不存在"))
}
return blockchainService.queryUsdceBalance(account.proxyAddress)
}
}
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Service
/**
* WCOL 解包轮询任务
* 每 20 秒轮询一次,遍历所有账户的代理地址:若 WCOL 余额 > 0 则解包为 USDC.e
* 每 20 秒轮询一次,遍历所有账户的代理地址:若 WCOL 余额 > 0 则执行解包。
* 同一时间仅允许单次执行;若上次执行未结束则本次忽略(与现有轮询逻辑一致)。
* 若未配置 Builder API Key,直接跳过本轮(解包依赖 Relayer Gasless,未配置则无法执行)。
*/
@@ -408,10 +408,25 @@ class BacktestExecutionService(
val cost = actualSellQuantity.multiply(position.avgPrice)
val profitLoss = netAmount.subtract(cost)
// 更新余额和持仓
// Bug #39 Fix: correctly reduce position quantity after sell
currentBalance += netAmount
if (position.quantity <= BigDecimal.ZERO) {
val remainingQuantity = position.quantity - actualSellQuantity
val remainingLeaderBuyQuantity = if (position.leaderBuyQuantity != null && position.leaderBuyQuantity > BigDecimal.ZERO) {
val totalQty = position.quantity
val leaderReduction = actualSellQuantity.divide(
totalQty, 8, java.math.RoundingMode.DOWN
)
(position.leaderBuyQuantity - leaderReduction).coerceAtLeast(BigDecimal.ZERO)
} else {
position.leaderBuyQuantity
}
if (remainingQuantity <= BigDecimal.ZERO) {
positions.remove(positionKey)
} else {
positions[positionKey] = position.copy(
quantity = remainingQuantity,
leaderBuyQuantity = remainingLeaderBuyQuantity
)
}
// 记录交易到当前页列表
@@ -632,7 +647,8 @@ class BacktestExecutionService(
val settlementPrice = avgPrice
val settlementValue = quantity.multiply(settlementPrice)
val profitLoss = settlementValue.negate()
// Bug #39 Fix: profitLoss for closed settlement at avgPrice should be ~0
val profitLoss = settlementValue.subtract(quantity.multiply(avgPrice))
balance += settlementValue
@@ -702,7 +718,8 @@ class BacktestExecutionService(
if (balance > peakBalance) {
peakBalance = balance
}
val drawdown = peakBalance - runningBalance
// Bug #39 Fix: use current balance, not runningBalance from previous iteration
val drawdown = peakBalance - balance
if (drawdown > maxDrawdown) {
maxDrawdown = drawdown
}
@@ -39,8 +39,8 @@ class BlockchainService(
private val logger = LoggerFactory.getLogger(BlockchainService::class.java)
// USDC 合约地址(Polygon 主网,Polymarket 使用 Polygon
private val usdcContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
// pUSD 合约地址(Polygon 主网,Polymarket 使用 Polygon
private val usdcContractAddress = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
// Polymarket Safe 代理工厂合约地址(Polygon 主网,用于 MetaMask 用户)
// 合约地址: 0xaacFeEa03eb1561C4e67d661e40682Bd20E3541b
@@ -56,7 +56,7 @@ class BlockchainService(
// ConditionalTokens 合约地址(Polygon 主网)
private val conditionalTokensAddress = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
// Neg Risk WrappedCollateral 合约地址(Polygon,解包后得 USDC.e
// Neg Risk WrappedCollateral 合约地址(Polygon
private val wcolContractAddress = "0x3A3BD7bb9528E159577F7C2e685CC81A765002E2"
// 空集合ID(用于计算collectionId
@@ -812,11 +812,11 @@ class BlockchainService(
}
/**
* 将代理钱包内的 WCOL 解包为 USDC.e(解包后转入代理地址)
* 赎回 Neg Risk 仓位后到账为 WCOL,调用此方法可转为 USDC.e 以便显示/使用
* 将代理钱包内的 WCOL 执行解包(解包后转入代理地址)
* 赎回 Neg Risk 仓位后到账为 WCOL,调用此方法可执行解包后续资产处理
*
* Safe 与 Magic 使用同一套逻辑:同一 [createUnwrapWcolTx] + [RelayClientService.execute]
* Safe 走 execTransactionMagic 走 PROXY 编码,最终均为代理合约调用 WCOL.unwrap(proxyAddress, amount)USDC.e 转入 proxyAddress
* Safe 走 execTransactionMagic 走 PROXY 编码,最终均为代理合约调用 WCOL.unwrap(proxyAddress, amount)。
*
* @param privateKey 主钱包私钥
* @param proxyAddress 代理地址(Safe 或 Magic 代理)
@@ -855,6 +855,99 @@ class BlockchainService(
}
}
// USDC.e 合约地址(仅用于 wrap 查询)
private val usdceContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
/**
* 查询 USDC.e 余额(用于 wrap 前检查)
*/
suspend fun queryUsdceBalance(walletAddress: String): Result<BigDecimal> {
return try {
val rpcApi = polygonRpcApi
val functionSelector = "0x70a08231"
val paddedAddress = walletAddress.removePrefix("0x").lowercase().padStart(64, '0')
val data = functionSelector + paddedAddress
val rpcRequest = JsonRpcRequest(
method = "eth_call",
params = listOf(mapOf("to" to usdceContractAddress, "data" to data), "latest")
)
val response = rpcApi.call(rpcRequest)
if (!response.isSuccessful || response.body() == null) {
return Result.failure(Exception("RPC 请求失败"))
}
val hexBalance = response.body()!!.result?.asString ?: return Result.failure(Exception("result 为空"))
val balanceWei = BigInteger(hexBalance.removePrefix("0x"), 16)
Result.success(BigDecimal(balanceWei).divide(BigDecimal("1000000")))
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* 将 USDC.e wrap 为 pUSD
* 步骤:1) 检查 USDC.e 余额 → 2) approve CollateralOnramp → 3) wrap
*/
suspend fun wrapUsdcToPusd(
privateKey: String,
proxyAddress: String,
walletType: WalletType
): Result<String?> {
return try {
val balanceResult = queryUsdceBalance(proxyAddress)
val balance = balanceResult.getOrElse {
logger.warn("查询 USDC.e 余额失败: ${it.message}")
return Result.failure(it)
}
if (balance <= BigDecimal.ZERO) {
return Result.success(null)
}
val wrapAmountWei = balance.movePointRight(6).toBigInteger()
logger.info("开始 wrap USDC.e → pUSD: proxy=${proxyAddress.take(10)}..., amount=$balance")
val unlimitedAllowance = BigInteger.valueOf(2).pow(256).minus(BigInteger.ONE)
val approveTx = relayClientService.createUsdceApproveForWrapTx(unlimitedAllowance)
val wrapTx = relayClientService.createWrapToPusdTx(proxyAddress, wrapAmountWei)
if (walletType == WalletType.MAGIC) {
// MAGIC 账户走 PROXY 时,不使用 Safe MultiSend(delegatecall)
// 改为顺序执行两笔 CALL,避免内层 delegatecall 回滚导致“外层成功但业务失败”。
val approveResult = relayClientService.execute(privateKey, proxyAddress, approveTx, walletType)
val approveHash = approveResult.getOrElse {
logger.error("USDC.e approve 失败: ${it.message}", it)
return Result.failure(it)
}
logger.info("USDC.e approve 成功: txHash=$approveHash")
val wrapResult = relayClientService.execute(privateKey, proxyAddress, wrapTx, walletType)
wrapResult.fold(
onSuccess = { txHash ->
logger.info("USDC.e → pUSD wrap 成功: txHash=$txHash")
Result.success(txHash)
},
onFailure = { e ->
logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e)
Result.failure(e)
}
)
} else {
val safeTx = relayClientService.createMultiSendTx(listOf(approveTx, wrapTx))
val executeResult = relayClientService.execute(privateKey, proxyAddress, safeTx, walletType)
executeResult.fold(
onSuccess = { txHash ->
logger.info("USDC.e → pUSD wrap 成功: txHash=$txHash")
Result.success(txHash)
},
onFailure = { e ->
logger.error("USDC.e → pUSD wrap 失败: ${e.message}", e)
Result.failure(e)
}
)
}
} catch (e: Exception) {
logger.error("USDC.e → pUSD wrap 异常: ${e.message}", e)
Result.failure(e)
}
}
/**
* 获取代理钱包的 nonce(用于构建 Safe 交易)
*/
@@ -223,15 +223,7 @@ class PolymarketClobService(
}
}
/**
* 创建订单已废弃使用 createSignedOrder 代替
* @deprecated 使用 createSignedOrder 代替需要签名的订单对象
*/
@Deprecated("使用 createSignedOrder 代替")
suspend fun createOrder(request: CreateOrderRequest): Result<OrderResponse> {
return Result.failure(UnsupportedOperationException("已废弃,请使用 createSignedOrder 方法"))
}
/**
* 创建签名的订单
* 注意此方法需要完整的订单签名逻辑当前为占位实现
@@ -45,7 +45,9 @@ object OnChainWsUtils {
}
// 合约地址
const val USDC_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
const val PUSD_CONTRACT = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" // V2 pUSD
const val USDC_CONTRACT = PUSD_CONTRACT // 默认使用 pUSD
private val COLLATERAL_CONTRACTS = setOf(PUSD_CONTRACT.lowercase())
const val ERC1155_CONTRACT = "0x4d97dcd97ec945f40cf65f87097ace5ea0476045"
const val ERC20_TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
const val ERC1155_TRANSFER_SINGLE_TOPIC = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"
@@ -96,8 +98,8 @@ object OnChainWsUtils {
val t0 = topics[0].lowercase()
val data = log.get("data")?.asString ?: "0x"
// USDC ERC20 Transfer
if (address == USDC_CONTRACT.lowercase() && t0 == ERC20_TRANSFER_TOPIC && topics.size >= 3) {
// 抵押品 ERC20 Transfer(当前仅匹配 pUSD
if (address in COLLATERAL_CONTRACTS && t0 == ERC20_TRANSFER_TOPIC && topics.size >= 3) {
val from = topicToAddress(topics[1])
val to = topicToAddress(topics[2])
val value = hexToBigInt(data)
@@ -149,6 +151,42 @@ object OnChainWsUtils {
return Pair(erc20, erc1155)
}
/**
* 通过 CLOB 交易历史获取 Leader 真实成交价用于无 USDC 转账的 ERC1155-only 场景
* 查询该 token 最近的成交记录匹配 side 方向的第一条作为成交价
* 返回 usdcRaw = price × sizeRaw
*/
private suspend fun fetchEstimatedUsdcRaw(
tokenId: String,
side: String,
sizeRaw: BigInteger,
retrofitFactory: RetrofitFactory
): BigInteger? {
return try {
val clobApi = retrofitFactory.createClobApiWithoutAuth()
val response = clobApi.getTrades(asset_id = tokenId)
if (!response.isSuccessful || response.body() == null) {
logger.warn("CLOB 交易历史查询失败: tokenId=$tokenId, code=${response.code()}")
return null
}
val trades = response.body()!!.data
val clobSide = side.lowercase()
val matchedTrade = trades.firstOrNull { it.side.equals(clobSide, ignoreCase = true) }
if (matchedTrade == null) {
logger.warn("CLOB 交易历史中未找到匹配成交: tokenId=$tokenId, side=$clobSide, totalTrades=${trades.size}")
return null
}
val price = matchedTrade.price.toBigDecimal()
val usdcRaw = price.multiply(sizeRaw.toBigDecimal())
.setScale(0, java.math.RoundingMode.DOWN).toBigInteger()
logger.debug("CLOB 交易历史估算: tokenId=$tokenId, side=$clobSide, tradePrice=${matchedTrade.price}, tradeSize=${matchedTrade.size}, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw")
usdcRaw
} catch (e: Exception) {
logger.warn("获取 CLOB 交易历史失败: tokenId=$tokenId, side=$side, error=${e.message}")
null
}
}
/**
* Transfer 日志解析交易信息
*/
@@ -205,6 +243,32 @@ object OnChainWsUtils {
asset = bestOutId
sizeRaw = bestOutVal
usdcRaw = usdcIn
} else if (bestInId != null && bestInVal > BigInteger.ZERO && bestOutId == null
&& usdcOut == BigInteger.ZERO && usdcIn == BigInteger.ZERO
) {
// BUY(无 USDC: 只收到 ERC1155 token,无 USDC 流动(CLOB 内部结算等场景)
side = "BUY"
asset = bestInId
sizeRaw = bestInVal
usdcRaw = fetchEstimatedUsdcRaw(bestInId.toString(), "BUY", bestInVal, retrofitFactory)
?: run {
logger.warn("无法获取估算价格(ERC1155-only BUY: txHash=$txHash, tokenId=$bestInId")
return null
}
logger.debug("ERC1155-only BUY: txHash=$txHash, tokenId=$bestInId, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw")
} else if (bestOutId != null && bestOutVal > BigInteger.ZERO && bestInId == null
&& usdcOut == BigInteger.ZERO && usdcIn == BigInteger.ZERO
) {
// SELL(无 USDC: 只发出 ERC1155 token,无 USDC 流动
side = "SELL"
asset = bestOutId
sizeRaw = bestOutVal
usdcRaw = fetchEstimatedUsdcRaw(bestOutId.toString(), "SELL", bestOutVal, retrofitFactory)
?: run {
logger.warn("无法获取估算价格(ERC1155-only SELL: txHash=$txHash, tokenId=$bestOutId")
return null
}
logger.debug("ERC1155-only SELL: txHash=$txHash, tokenId=$bestOutId, sizeRaw=$sizeRaw, usdcRaw=$usdcRaw")
} else {
// 无法判断交易方向
logger.debug("无法判断交易方向: txHash=$txHash, bestInId=$bestInId, bestInVal=$bestInVal, bestOutId=$bestOutId, bestOutVal=$bestOutVal, usdcOut=$usdcOut, usdcIn=$usdcIn")
@@ -41,10 +41,9 @@ class OrderSigningService {
return if (walletTypeEnum == com.wrbug.polymarketbot.enums.WalletType.MAGIC) 1 else 2
}
// Polygon 主网合约地址(标准 CTF Exchange
private val EXCHANGE_CONTRACT = "0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E"
// Neg Risk CTF Exchangeneg risk 市场需用此合约签约,否则服务端返回 invalid signature
private val NEG_RISK_EXCHANGE_CONTRACT = "0xC5d563A36AE78145C45a50134d48A1215220f80a"
// V2 合约地址
private val EXCHANGE_CONTRACT = "0xE111180000d2663C0091e4f400237545B87B996B"
private val NEG_RISK_EXCHANGE_CONTRACT = "0xe2222d279d744050d28e00520010520000310F59"
private val CHAIN_ID = 137L
// USDC 有 6 位小数
@@ -156,8 +155,8 @@ class OrderSigningService {
}
/**
* 创建并签名订单
*
* 创建并签名订单 (V2)
*
* @param privateKey 私钥十六进制字符串
* @param makerAddress maker 地址funder通常是 proxyAddress
* @param tokenId token ID
@@ -165,9 +164,6 @@ class OrderSigningService {
* @param price 价格
* @param size 数量
* @param signatureType 签名类型1: Email/Magic, 2: Browser Wallet, 0: EOA
* @param nonce nonce默认 "0"
* @param feeRateBps 费率基点默认 "0"
* @param expiration 过期时间戳0 表示永不过期
* @param exchangeContract 签约用 exchange 合约地址null 时用标准 CTF Exchangeneg risk 市场需传 Neg Risk Exchange
* @return 签名的订单对象
*/
@@ -178,10 +174,7 @@ class OrderSigningService {
side: String,
price: String,
size: String,
signatureType: Int = 2, // 默认使用 Browser Wallet(与正确订单数据一致)
nonce: String = "0",
feeRateBps: String = "0",
expiration: String = "0",
signatureType: Int = 2,
exchangeContract: String? = null
): SignedOrderObject {
try {
@@ -189,33 +182,32 @@ class OrderSigningService {
val cleanPrivateKey = privateKey.removePrefix("0x")
val privateKeyBigInt = BigInteger(cleanPrivateKey, 16)
val credentials = Credentials.create(privateKeyBigInt.toString(16))
// 统一转换为小写,确保与 EIP-712 编码时使用的地址格式一致
// EIP-712 编码时地址会被转换为小写,所以订单对象中的地址也应该是小写
val signerAddress = credentials.address.lowercase()
// 2. 计算订单金额
val amounts = calculateOrderAmounts(side, size, price)
// 3. 生成 salt(使用时间戳,毫秒
// 3. 生成 salt 和 timestampV2: timestamp 替代 nonce 保证唯一性
val salt = generateSalt()
// 4. taker 地址(默认使用零地址)
val taker = "0x0000000000000000000000000000000000000000"
val timestamp = System.currentTimeMillis().toString()
// 4. V2 字段默认值
val metadata = "0x0000000000000000000000000000000000000000000000000000000000000000"
val builder = "0x0000000000000000000000000000000000000000000000000000000000000000"
// 5. 确保 maker 地址也是小写格式
val makerAddressLower = makerAddress.lowercase()
// 打印签名前的订单参数(DEBUG 级别,避免敏感信息泄露)
logger.debug("========== 订单签名前参数 ==========")
logger.debug("========== 订单签名前参数 (V2) ==========")
logger.debug("订单方向: $side, 价格: $price, 数量: $size")
logger.debug("Token ID: $tokenId")
logger.debug("Maker: ${makerAddressLower.take(10)}...${makerAddressLower.takeLast(6)}")
logger.debug("Signer: ${signerAddress.take(10)}...${signerAddress.takeLast(6)}")
logger.debug("Amounts - Maker: ${amounts.makerAmount}, Taker: ${amounts.takerAmount}")
logger.debug("Salt: $salt, Expiration: $expiration, Nonce: $nonce, FeeRateBPS: $feeRateBps")
logger.debug("Salt: $salt, Timestamp: $timestamp")
logger.debug("Signature Type: $signatureType, Chain ID: $CHAIN_ID")
// 6. 构建订单数据并签名neg risk 市场需用 NEG_RISK_EXCHANGE_CONTRACT
// 6. 构建订单数据并签名
val contract = exchangeContract?.takeIf { it.isNotBlank() } ?: EXCHANGE_CONTRACT
val signature = signOrder(
privateKey = privateKey,
@@ -224,44 +216,41 @@ class OrderSigningService {
salt = salt,
maker = makerAddressLower,
signer = signerAddress,
taker = taker,
tokenId = tokenId,
makerAmount = amounts.makerAmount,
takerAmount = amounts.takerAmount,
expiration = expiration,
nonce = nonce,
feeRateBps = feeRateBps,
side = side.uppercase(),
signatureType = signatureType
signatureType = signatureType,
timestamp = timestamp,
metadata = metadata,
builder = builder
)
// 7. 创建签名订单对象
// 注意:所有地址字段都使用小写格式,确保与签名时使用的地址一致
// 7. 创建 V2 签名订单对象
return SignedOrderObject(
salt = salt,
maker = makerAddressLower,
signer = signerAddress,
taker = taker,
taker = "0x0000000000000000000000000000000000000000",
tokenId = tokenId,
makerAmount = amounts.makerAmount,
takerAmount = amounts.takerAmount,
expiration = expiration,
nonce = nonce,
feeRateBps = feeRateBps,
side = side.uppercase(),
signatureType = signatureType,
timestamp = timestamp,
expiration = "0",
metadata = metadata,
builder = builder,
signature = signature
)
} catch (e: Exception) {
logger.error("创建并签名订单失败", e)
throw RuntimeException("创建并签名订单失败: ${e.message}", e)
logger.error("创建并签名订单失败 (V2)", e)
throw RuntimeException("创建并签名订单失败 (V2): ${e.message}", e)
}
}
/**
* 签名订单EIP-712
*
* 参考: @polymarket/order-utils ExchangeOrderBuilder
* 签名订单 V2EIP-712
*/
private fun signOrder(
privateKey: String,
@@ -270,56 +259,47 @@ class OrderSigningService {
salt: Long,
maker: String,
signer: String,
taker: String,
tokenId: String,
makerAmount: String,
takerAmount: String,
expiration: String,
nonce: String,
feeRateBps: String,
side: String,
signatureType: Int
signatureType: Int,
timestamp: String,
metadata: String,
builder: String
): String {
try {
// 1. 私钥与密钥对
val cleanPrivateKey = privateKey.removePrefix("0x")
val privateKeyBigInt = BigInteger(cleanPrivateKey, 16)
val credentials = Credentials.create(privateKeyBigInt.toString(16))
val ecKeyPair = credentials.ecKeyPair
// 2. 编码域分隔符(verifyingContract 显式小写,与 EIP-712 约定一致)
val domainSeparator = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeDomain(
chainId = chainId,
verifyingContract = exchangeContract.lowercase()
)
// 3. 编码订单消息哈希
// signatureType1 = POLY_PROXY (Magic), 2 = POLY_GNOSIS_SAFE (Safe), 0 = EOA
val orderHash = com.wrbug.polymarketbot.util.Eip712Encoder.encodeExchangeOrder(
salt = salt,
maker = maker,
signer = signer,
taker = taker,
tokenId = tokenId,
makerAmount = makerAmount,
takerAmount = takerAmount,
expiration = expiration,
nonce = nonce,
feeRateBps = feeRateBps,
side = side,
signatureType = signatureType
signatureType = signatureType,
timestamp = timestamp,
metadata = metadata,
builder = builder
)
// 4. 计算完整 EIP-712 结构化数据哈希
val structuredHash = com.wrbug.polymarketbot.util.Eip712Encoder.hashStructuredData(
domainSeparator = domainSeparator,
messageHash = orderHash
)
// 5. 使用私钥签名(needToHash=false,对 32 字节 hash 直接签名)
val signature = org.web3j.crypto.Sign.signMessage(structuredHash, ecKeyPair, false)
// 6. 组合 r + s + v
val rHex = org.web3j.utils.Numeric.toHexString(signature.r).removePrefix("0x").padStart(64, '0')
val sHex = org.web3j.utils.Numeric.toHexString(signature.s).removePrefix("0x").padStart(64, '0')
val vBytes = signature.v
@@ -328,8 +308,8 @@ class OrderSigningService {
return "0x$rHex$sHex$vHex"
} catch (e: Exception) {
logger.error("订单签名失败", e)
throw RuntimeException("订单签名失败: ${e.message}", e)
logger.error("订单签名失败 (V2)", e)
throw RuntimeException("订单签名失败 (V2): ${e.message}", e)
}
}
@@ -562,16 +562,7 @@ open class CopyOrderTrackingService(
// 解密私钥
val decryptedPrivateKey = decryptPrivateKey(account)
// 获取费率(根据 Polymarket Maker Rebates Program 要求)
val feeRateResult = clobService.getFeeRate(tokenId)
val feeRateBps = if (feeRateResult.isSuccess) {
feeRateResult.getOrNull()?.toString() ?: "0"
} else {
logger.warn("获取费率失败,使用默认值 0: tokenId=$tokenId, error=${feeRateResult.exceptionOrNull()?.message}")
"0"
}
logger.info("准备创建买入订单: copyTradingId=${copyTrading.id}, tradeId=${trade.id}, leaderPrice=${trade.price}, tolerance=${copyTrading.priceTolerance}, calculatedPrice=$buyPrice, quantity=$finalBuyQuantity, baseFee=$feeRateBps")
logger.info("准备创建买入订单: copyTradingId=${copyTrading.id}, tradeId=${trade.id}, leaderPrice=${trade.price}, tolerance=${copyTrading.priceTolerance}, calculatedPrice=$buyPrice, quantity=$finalBuyQuantity")
// Neg Risk 市场需用 Neg Risk Exchange 签约,否则服务端返回 invalid signature
val negRisk = marketService.getNegRiskByConditionId(effectiveMarketId) == true
@@ -594,7 +585,6 @@ open class CopyOrderTrackingService(
owner = account.apiKey,
copyTradingId = copyTrading.id!!,
tradeId = trade.id,
feeRateBps = feeRateBps,
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
)
@@ -1018,15 +1008,6 @@ open class CopyOrderTrackingService(
// 8. 解密私钥(在方法开始时解密一次,后续复用)
val decryptedPrivateKey = decryptPrivateKey(account)
// 获取费率(根据 Polymarket Maker Rebates Program 要求)
val feeRateResult = clobService.getFeeRate(tokenId)
val feeRateBps = if (feeRateResult.isSuccess) {
feeRateResult.getOrNull()?.toString() ?: "0"
} else {
logger.warn("获取费率失败,使用默认值 0: tokenId=$tokenId, error=${feeRateResult.exceptionOrNull()?.message}")
"0"
}
// 9. Neg Risk 市场需用 Neg Risk Exchange 签约
val negRiskSell = marketService.getNegRiskByConditionId(leaderSellTrade.market) == true
val exchangeContractSell = orderSigningService.getExchangeContract(negRiskSell)
@@ -1042,9 +1023,6 @@ open class CopyOrderTrackingService(
price = sellPrice.toString(),
size = totalMatched.toString(),
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType),
nonce = "0",
feeRateBps = feeRateBps, // 使用动态获取的费率
expiration = "0",
exchangeContract = exchangeContractSell
)
} catch (e: Exception) {
@@ -1058,8 +1036,7 @@ open class CopyOrderTrackingService(
val orderRequest = NewOrderRequest(
order = signedOrder,
owner = account.apiKey,
orderType = "FAK", // Fill-And-Kill
deferExec = false
orderType = "FAK" // Fill-And-Kill
)
// 12. 创建带认证的CLOB API客户端(使用解密后的凭证)
@@ -1084,7 +1061,6 @@ open class CopyOrderTrackingService(
owner = account.apiKey,
copyTradingId = copyTrading.id,
tradeId = leaderSellTrade.id,
feeRateBps = feeRateBps,
signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
)
@@ -1179,7 +1155,6 @@ open class CopyOrderTrackingService(
* @param owner API Key用于owner字段
* @param copyTradingId 跟单配置ID用于日志
* @param tradeId Leader 交易ID用于日志
* @param feeRateBps 费率基点从API动态获取
* @param signatureType 签名类型1=Magic, 2=Safe
* @return 成功返回订单ID失败返回异常
*/
@@ -1196,7 +1171,6 @@ open class CopyOrderTrackingService(
owner: String,
copyTradingId: Long,
tradeId: String,
feeRateBps: String,
signatureType: Int
): Result<String> {
var lastError: Exception? = null
@@ -1213,9 +1187,6 @@ open class CopyOrderTrackingService(
price = price,
size = size,
signatureType = signatureType,
nonce = "0",
feeRateBps = feeRateBps, // 使用动态获取的费率
expiration = "0",
exchangeContract = exchangeContract
)
@@ -1232,8 +1203,7 @@ open class CopyOrderTrackingService(
val orderRequest = NewOrderRequest(
order = signedOrder,
owner = owner,
orderType = "FAK", // Fill-And-Kill
deferExec = false
orderType = "FAK" // Fill-And-Kill
)
// 调用 API 创建订单
@@ -815,9 +815,11 @@ class OrderStatusUpdateService(
val actualPrice = orderDetail.price?.toSafeBigDecimal() ?: order.price
val actualSize = orderDetail.originalSize?.toSafeBigDecimal() ?: order.quantity
val actualOutcome = orderDetail.outcome
// 使用交易所订单的实际创建时间(API返回秒级,转为毫秒)
val actualCreatedAt = if (orderDetail.createdAt > 0) orderDetail.createdAt * 1000 else order.createdAt
// 更新订单数据(如果实际数据与临时数据不同)
val needUpdate = actualPrice != order.price || actualSize != order.quantity
val needUpdate = actualPrice != order.price || actualSize != order.quantity || actualCreatedAt != order.createdAt
// 先保存更新后的订单,标记 notificationSent = true
// 这样可以防止其他并发任务重复发送通知
@@ -838,7 +840,7 @@ class OrderStatusUpdateService(
status = order.status,
notificationSent = true, // 标记为已发送通知
source = order.source, // 保留原始订单来源
createdAt = order.createdAt,
createdAt = actualCreatedAt,
updatedAt = System.currentTimeMillis()
)
@@ -46,6 +46,9 @@ private const val SPREAD_MAX_PRICE_ADJUSTMENT = "0.02"
/** 数量小数位数,与 OrderSigningService 的 roundConfig.size 一致 */
private const val SIZE_DECIMAL_SCALE = 2
/** 单笔下单最小 USDC 金额(平台限制),RATIO 模式计算值低于此值时按此值下单 */
private val MIN_ORDER_USDC = BigDecimal("1")
/**
* 周期内预置上下文账户解密凭证费率签名类型CLOB 客户端不含预签订单
* 触发时 FIXED/RATIO 均按 outcomeIndex 计算 size 并签名提交
@@ -58,7 +61,6 @@ private data class PeriodContext(
val apiSecretDecrypted: String,
val apiPassphraseDecrypted: String,
val clobApi: PolymarketClobApi,
val feeRateByTokenId: Map<String, String>,
val signatureType: Int,
val tokenIds: List<String>,
val marketTitle: String?
@@ -149,12 +151,9 @@ class CryptoTailStrategyExecutionService(
}
val clobApi = retrofitFactory.createClobApi(account.apiKey, apiSecret, apiPassphrase, account.walletAddress)
val feeRateByTokenId = tokenIds.associate { tokenId ->
tokenId to (clobService.getFeeRate(tokenId).getOrNull()?.toString() ?: "0")
}
val signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
if (strategy.amountMode.uppercase() != "RATIO" && strategy.amountValue < BigDecimal("1")) return null
if (strategy.amountMode.uppercase() != "RATIO" && strategy.amountValue < MIN_ORDER_USDC) return null
val ctx = PeriodContext(
strategy = strategy,
@@ -164,7 +163,6 @@ class CryptoTailStrategyExecutionService(
apiSecretDecrypted = apiSecret,
apiPassphraseDecrypted = apiPassphrase,
clobApi = clobApi,
feeRateByTokenId = feeRateByTokenId,
signatureType = signatureType,
tokenIds = tokenIds,
marketTitle = marketTitle
@@ -337,29 +335,36 @@ class CryptoTailStrategyExecutionService(
val ctx = getOrInvalidatePeriodContext(strategy, periodStartUnix)
if (ctx != null) {
val amountUsdc = when (strategy.amountMode.uppercase()) {
var availableBalanceForRatio = BigDecimal.ZERO
var amountUsdc = when (strategy.amountMode.uppercase()) {
"RATIO" -> {
val balanceResult = accountService.getAccountBalance(ctx.account.id)
val availableBalance =
balanceResult.getOrNull()?.availableBalance?.toSafeBigDecimal() ?: BigDecimal.ZERO
availableBalanceForRatio = availableBalance
availableBalance.multiply(strategy.amountValue).divide(BigDecimal("100"), 18, RoundingMode.DOWN)
}
else -> strategy.amountValue
}
if (amountUsdc < BigDecimal("1")) {
saveTriggerRecord(
strategy,
periodStartUnix,
marketTitle,
outcomeIndex,
triggerPrice,
amountUsdc,
null,
"fail",
"投入金额不足"
)
return
if (amountUsdc < MIN_ORDER_USDC) {
val amountMode = strategy.amountMode.uppercase()
if (amountMode == "RATIO" && availableBalanceForRatio >= MIN_ORDER_USDC) {
amountUsdc = MIN_ORDER_USDC
} else {
saveTriggerRecord(
strategy,
periodStartUnix,
marketTitle,
outcomeIndex,
triggerPrice,
amountUsdc,
null,
"fail",
"投入金额不足"
)
return
}
}
val tokenId = tokenIds.getOrNull(outcomeIndex) ?: run {
@@ -387,7 +392,6 @@ class CryptoTailStrategyExecutionService(
}
val priceStr = price.toPlainString()
val size = computeSize(amountUsdc, price)
val feeRateBps = ctx.feeRateByTokenId[tokenId] ?: "0"
val signedOrder = orderSigningService.createAndSignOrder(
privateKey = ctx.decryptedPrivateKey,
makerAddress = ctx.account.proxyAddress,
@@ -395,16 +399,12 @@ class CryptoTailStrategyExecutionService(
side = "BUY",
price = priceStr,
size = size,
signatureType = ctx.signatureType,
nonce = "0",
feeRateBps = feeRateBps,
expiration = "0"
signatureType = ctx.signatureType
)
val orderRequest = NewOrderRequest(
order = signedOrder,
owner = ctx.account.apiKey!!,
orderType = "FAK",
deferExec = false
orderType = "FAK"
)
submitOrderAndSaveRecord(
ctx.clobApi,
@@ -521,23 +521,28 @@ class CryptoTailStrategyExecutionService(
val balanceResult = accountService.getAccountBalance(account.id)
val availableBalance = balanceResult.getOrNull()?.availableBalance?.toSafeBigDecimal() ?: BigDecimal.ZERO
val amountUsdc = when (strategy.amountMode.uppercase()) {
var amountUsdc = when (strategy.amountMode.uppercase()) {
"RATIO" -> availableBalance.multiply(strategy.amountValue).divide(BigDecimal("100"), 18, RoundingMode.DOWN)
else -> strategy.amountValue
}
if (amountUsdc < BigDecimal("1")) {
saveTriggerRecord(
strategy,
periodStartUnix,
marketTitle,
outcomeIndex,
triggerPrice,
amountUsdc,
null,
"fail",
"投入金额不足"
)
return
if (amountUsdc < MIN_ORDER_USDC) {
val amountMode = strategy.amountMode.uppercase()
if (amountMode == "RATIO" && availableBalance >= MIN_ORDER_USDC) {
amountUsdc = MIN_ORDER_USDC
} else {
saveTriggerRecord(
strategy,
periodStartUnix,
marketTitle,
outcomeIndex,
triggerPrice,
amountUsdc,
null,
"fail",
"投入金额不足"
)
return
}
}
val tokenId = tokenIds.getOrNull(outcomeIndex) ?: run {
@@ -594,7 +599,6 @@ class CryptoTailStrategyExecutionService(
""
}
val clobApi = retrofitFactory.createClobApi(account.apiKey, apiSecret, apiPassphrase, account.walletAddress)
val feeRateBps = clobService.getFeeRate(tokenId).getOrNull()?.toString() ?: "0"
val signatureType = orderSigningService.getSignatureTypeForWalletType(account.walletType)
val signedOrder = orderSigningService.createAndSignOrder(
@@ -604,16 +608,12 @@ class CryptoTailStrategyExecutionService(
side = "BUY",
price = priceStr,
size = size,
signatureType = signatureType,
nonce = "0",
feeRateBps = feeRateBps,
expiration = "0"
signatureType = signatureType
)
val orderRequest = NewOrderRequest(
order = signedOrder,
owner = account.apiKey!!,
orderType = "FAK",
deferExec = false
orderType = "FAK"
)
submitOrderAndSaveRecord(
clobApi,
@@ -702,7 +702,7 @@ class CryptoTailStrategyExecutionService(
val amountUsdc = priceRounded.multi(size).setScale(2, RoundingMode.HALF_UP)
if (amountUsdc < BigDecimal.ONE) {
return Result.failure(IllegalArgumentException("总金额不能少于 1 USDC"))
return Result.failure(IllegalArgumentException("总金额不能少于 \$1"))
}
val mutex = getTriggerMutex(strategy.id!!, request.periodStartUnix)
@@ -730,7 +730,6 @@ class CryptoTailStrategyExecutionService(
val priceStr = priceRounded.toPlainString()
val sizeStr = size.toPlainString()
val feeRateBps = ctx.feeRateByTokenId[tokenId] ?: "0"
val signedOrder = orderSigningService.createAndSignOrder(
privateKey = ctx.decryptedPrivateKey,
@@ -739,17 +738,13 @@ class CryptoTailStrategyExecutionService(
side = "BUY",
price = priceStr,
size = sizeStr,
signatureType = ctx.signatureType,
nonce = "0",
feeRateBps = feeRateBps,
expiration = "0"
signatureType = ctx.signatureType
)
val orderRequest = NewOrderRequest(
order = signedOrder,
owner = ctx.account.apiKey!!,
orderType = "FAK",
deferExec = false
orderType = "FAK"
)
val orderResult = submitOrderForManualOrder(
@@ -169,9 +169,9 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
可用余额: <code>{{available_balance}}</code> USDC
可用余额: <code>${'$'}{{available_balance}}</code>
时间: <code>{{time}}</code>
""".trimIndent(),
@@ -184,7 +184,7 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
<b>错误信息</b>
@@ -201,7 +201,7 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
<b>过滤类型</b> <code>{{filter_type}}</code>
@@ -222,7 +222,7 @@ class NotificationTemplateService(
方向: <b>{{side}}</b>
价格: <code>{{price}}</code>
数量: <code>{{quantity}}</code> shares
金额: <code>{{amount}}</code> USDC
金额: <code>${'$'}{{amount}}</code>
账户: {{account_name}}
时间: <code>{{time}}</code>
@@ -233,8 +233,8 @@ class NotificationTemplateService(
📊 <b>赎回信息</b>
账户: {{account_name}}
交易哈希: <code>{{transaction_hash}}</code>
赎回总价值: <code>{{total_value}}</code> USDC
可用余额: <code>{{available_balance}}</code> USDC
赎回总价值: <code>${'$'}{{total_value}}</code>
可用余额: <code>${'$'}{{available_balance}}</code>
时间: <code>{{time}}</code>
""".trimIndent(),
@@ -246,7 +246,7 @@ class NotificationTemplateService(
账户: {{account_name}}
交易哈希: <code>{{transaction_hash}}</code>
可用余额: <code>{{available_balance}}</code> USDC
可用余额: <code>${'$'}{{available_balance}}</code>
时间: <code>{{time}}</code>
""".trimIndent()
@@ -39,8 +39,14 @@ class RelayClientService(
// ConditionalTokens 合约地址
private val conditionalTokensAddress = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045"
// USDC.e 合约地址(普通市场抵押品)
private val usdcContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
// pUSD 合约地址(普通市场抵押品)
private val usdcContractAddress = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB"
// USDC.e 合约地址(仅用于 wrap 到 pUSD)
private val usdceContractAddress = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"
// CollateralOnramp 合约地址(USDC.e → pUSD
private val collateralOnrampAddress = "0x93070a847efEf7F70739046A929D47a521F5B8ee"
// Neg Risk 市场使用的 WrappedCollateral 合约地址(Polygonneg-risk-ctf-adapter
private val negRiskWrappedCollateralAddress = "0x3A3BD7bb9528E159577F7C2e685CC81A765002E2"
@@ -51,7 +57,9 @@ class RelayClientService(
// Polygon PROXYMagic)合约地址,参考 builder-relayer-client config
private val proxyFactoryAddress = "0xaB45c5A4B0c941a2F231C04C3f49182e1A254052"
private val relayHubAddress = "0xD216153c06E857cD7f72665E0aF1d7D82172F494"
private val defaultProxyGasLimit = "10000000"
// PROXY relayCall 内层 gasLimit(签名参数)不能给过大值,否则 RelayHub 会因 gasleft 校验失败回滚。
private val defaultProxyGasLimit = "2400000"
private val maxProxyGasLimit = BigInteger.valueOf(2400000)
// Safe MultiSend 合约地址(Polygon 主网)
private val safeMultisendAddress = "0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761"
@@ -282,14 +290,14 @@ class RelayClientService(
}
/**
* 创建 WCOL 解包交易 Wrapped Collateral 解包为 USDC.e
* 创建 WCOL 解包交易 Wrapped Collateral 执行解包
* 合约: Neg Risk WrappedCollateral 0x3A3BD7bb9528E159577F7C2e685CC81A765002E2
* 方法: unwrap(address _to, uint256 _amount)解包后 USDC.e 转到 _to
* 方法: unwrap(address _to, uint256 _amount)
*
* Safe Magic 共用此交易对象Safe [executeViaBuilderRelayer] / [executeManually]execTransaction
* Magic [executeViaBuilderRelayerProxy]encodeProxyTransactionData语义一致
*
* @param toAddress 接收 USDC.e 的地址通常为 proxy 自身使余额留在代理钱包
* @param toAddress 接收解包资产的地址通常为 proxy 自身使余额留在代理钱包
* @param amountWei WCOL 数量6 位小数对应的 raw balanceOf 返回一致
* @return Safe 交易对象
*/
@@ -323,6 +331,41 @@ class RelayClientService(
)
}
/**
* 创建 USDC.e approve 交易用于 wrap pUSD
* 授权 CollateralOnramp 合约花费用户的 USDC.e
*/
fun createUsdceApproveForWrapTx(amount: BigInteger): SafeTransaction {
val functionSelector = EthereumUtils.getFunctionSelector("approve(address,uint256)")
val encodedSpender = EthereumUtils.encodeAddress(collateralOnrampAddress)
val encodedAmount = EthereumUtils.encodeUint256(amount)
val callData = "0x" + functionSelector.removePrefix("0x") + encodedSpender + encodedAmount
return SafeTransaction(
to = usdceContractAddress,
operation = 0,
data = callData,
value = "0"
)
}
/**
* 创建 USDC.e pUSD wrap 交易
* CollateralOnramp.wrap(address _asset, address _to, uint256 _amount)
*/
fun createWrapToPusdTx(recipientAddress: String, amount: BigInteger): SafeTransaction {
val functionSelector = EthereumUtils.getFunctionSelector("wrap(address,address,uint256)")
val asset = EthereumUtils.encodeAddress(usdceContractAddress)
val to = EthereumUtils.encodeAddress(recipientAddress)
val amt = EthereumUtils.encodeUint256(amount)
val callData = "0x" + functionSelector.removePrefix("0x") + asset + to + amt
return SafeTransaction(
to = collateralOnrampAddress,
operation = 0,
data = callData,
value = "0"
)
}
/**
* 创建 MultiSend 交易合并多个 SafeTransaction 为一笔交易
* 参考 TypeScript: builder-relayer-client/src/encode/safe.ts createSafeMultisendTransaction
@@ -489,7 +532,16 @@ class RelayClientService(
// 估算 gas limit(参考 builder-relayer-client builder/proxy.ts getGasLimit
val gasLimit = try {
estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData)
val estimatedGasLimit = estimateProxyGasLimit(fromAddress, proxyFactoryAddress, proxyCallData)
val estimatedBigInt = BigInteger(estimatedGasLimit)
if (estimatedBigInt > maxProxyGasLimit) {
logger.warn(
"估算 PROXY gas limit 过大,进行截断: estimated=$estimatedGasLimit, capped=$maxProxyGasLimit"
)
maxProxyGasLimit.toString()
} else {
estimatedGasLimit
}
} catch (e: Exception) {
logger.warn("估算 PROXY gas limit 失败,使用默认值: ${e.message}", e)
defaultProxyGasLimit
@@ -691,7 +691,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo
<b>$filterTypeLabel</b> <code>$filterTypeDisplay</code>
@@ -1169,9 +1169,9 @@ class TelegramNotificationService(
} else {
balanceDecimal.stripTrailingZeros()
}
"\n$availableBalanceLabel: <code>${formatted.toPlainString()}</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}${formatted.toPlainString()}</code>"
} catch (e: Exception) {
"\n$availableBalanceLabel: <code>$availableBalance</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}$availableBalance</code>"
}
} else {
""
@@ -1185,7 +1185,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo$escapedCopyTradingInfo$availableBalanceDisplay
$timeLabel: <code>$time</code>"""
@@ -1264,7 +1264,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo
$timeLabel: <code>$time</code>"""
@@ -1381,7 +1381,7 @@ class TelegramNotificationService(
$sideLabel: <b>$sideDisplay</b>
$priceLabel: <code>$priceDisplay</code>
$quantityLabel: <code>$sizeDisplay</code> shares
$amountLabel: <code>$amountDisplay</code> USDC
$amountLabel: <code>${'$'}$amountDisplay</code>
$accountLabel: $escapedAccountInfo
<b>$errorInfo</b>
@@ -1515,7 +1515,7 @@ class TelegramNotificationService(
} catch (e: Exception) {
position.value
}
"${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares = $valueDisplay USDC"
"${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares = ${'$'}$valueDisplay"
}
// 格式化可用余额
@@ -1527,9 +1527,9 @@ class TelegramNotificationService(
} else {
balanceDecimal.stripTrailingZeros()
}
"\n$availableBalanceLabel: <code>${formatted.toPlainString()}</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}${formatted.toPlainString()}</code>"
} catch (e: Exception) {
"\n$availableBalanceLabel: <code>$availableBalance</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}$availableBalance</code>"
}
} else {
""
@@ -1540,7 +1540,7 @@ class TelegramNotificationService(
📊 <b>$redeemInfo</b>
$accountLabel: $escapedAccountInfo
$transactionHashLabel: <code>$escapedTxHash</code>
$totalValueLabel: <code>$totalValueDisplay</code> USDC$availableBalanceDisplay
$totalValueLabel: <code>${'$'}$totalValueDisplay</code>$availableBalanceDisplay
📦 <b>$positionsLabel</b>
$positionsText
@@ -1642,9 +1642,9 @@ $positionsText
} else {
balanceDecimal.stripTrailingZeros()
}
"\n$availableBalanceLabel: <code>${formatted.toPlainString()}</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}${formatted.toPlainString()}</code>"
} catch (e: Exception) {
"\n$availableBalanceLabel: <code>$availableBalance</code> USDC"
"\n$availableBalanceLabel: <code>${'$'}$availableBalance</code>"
}
} else {
""
@@ -167,9 +167,8 @@ object Eip712Encoder {
}
/**
* 编码 ExchangeOrder 域分隔符
* 参考: @polymarket/order-utils ExchangeOrderBuilder
* Domain: { name: "Polymarket CTF Exchange", version: "1", chainId: chainId, verifyingContract: exchangeContract }
* 编码 ExchangeOrder V2 域分隔符
* Domain: { name: "Polymarket CTF Exchange", version: "2", chainId: chainId, verifyingContract: exchangeContract }
*/
fun encodeExchangeDomain(
chainId: Long,
@@ -184,9 +183,9 @@ object Eip712Encoder {
"verifyingContract" to "address"
)
)
val nameHash = encodeString("Polymarket CTF Exchange")
val versionHash = encodeString("1")
val versionHash = encodeString("2")
val chainIdBytes = encodeUint256(BigInteger.valueOf(chainId))
val contractBytes = encodeAddress(verifyingContract)
@@ -201,23 +200,21 @@ object Eip712Encoder {
}
/**
* 编码 ExchangeOrder 消息哈希
* 参考: @polymarket/order-utils ExchangeOrderBuilder
* Order: { salt, maker, signer, taker, tokenId, makerAmount, takerAmount, expiration, nonce, feeRateBps, side, signatureType }
* 编码 ExchangeOrder V2 消息哈希
* V2 Order: { salt, maker, signer, tokenId, makerAmount, takerAmount, side, signatureType, timestamp, metadata, builder }
*/
fun encodeExchangeOrder(
salt: Long,
maker: String,
signer: String,
taker: String,
tokenId: String,
makerAmount: String,
takerAmount: String,
expiration: String,
nonce: String,
feeRateBps: String,
side: String,
signatureType: Int
signatureType: Int,
timestamp: String,
metadata: String,
builder: String
): ByteArray {
val orderTypeHash = encodeType(
"Order",
@@ -225,57 +222,51 @@ object Eip712Encoder {
"salt" to "uint256",
"maker" to "address",
"signer" to "address",
"taker" to "address",
"tokenId" to "uint256",
"makerAmount" to "uint256",
"takerAmount" to "uint256",
"expiration" to "uint256",
"nonce" to "uint256",
"feeRateBps" to "uint256",
"side" to "uint8",
"signatureType" to "uint8"
"signatureType" to "uint8",
"timestamp" to "uint256",
"metadata" to "bytes32",
"builder" to "bytes32"
)
)
// 编码订单字段
val saltBytes = encodeUint256(BigInteger.valueOf(salt))
val makerBytes = encodeAddress(maker)
val signerBytes = encodeAddress(signer)
val takerBytes = encodeAddress(taker)
val tokenIdBytes = encodeUint256(BigInteger(tokenId))
val makerAmountBytes = encodeUint256(BigInteger(makerAmount))
val takerAmountBytes = encodeUint256(BigInteger(takerAmount))
val expirationBytes = encodeUint256(BigInteger(expiration))
val nonceBytes = encodeUint256(BigInteger(nonce))
val feeRateBpsBytes = encodeUint256(BigInteger(feeRateBps))
// side: BUY = 0, SELL = 1 (uint8,但需要编码为 32 字节)
val sideValue = when (side.uppercase()) {
"BUY" -> 0
"SELL" -> 1
else -> throw IllegalArgumentException("side 必须是 BUY 或 SELL")
}
// uint8 类型,但 EIP-712 编码时仍需要 32 字节
val sideBytes = encodeUint256(BigInteger.valueOf(sideValue.toLong()))
val signatureTypeBytes = encodeUint256(BigInteger.valueOf(signatureType.toLong()))
// 组合所有字段
val encoded = ByteArray(32 * 13) // 13 个字段,每个 32 字节
val timestampBytes = encodeUint256(BigInteger(timestamp))
val metadataBytes = Numeric.hexStringToByteArray(metadata.removePrefix("0x").padStart(64, '0'))
val builderBytes = Numeric.hexStringToByteArray(builder.removePrefix("0x").padStart(64, '0'))
val encoded = ByteArray(32 * 12) // typeHash + 11 个字段
var offset = 0
System.arraycopy(orderTypeHash, 0, encoded, offset, 32); offset += 32
System.arraycopy(saltBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(makerBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(signerBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(takerBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(tokenIdBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(makerAmountBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(takerAmountBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(expirationBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(nonceBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(feeRateBpsBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(sideBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(signatureTypeBytes, 0, encoded, offset, 32)
System.arraycopy(signatureTypeBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(timestampBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(metadataBytes, 0, encoded, offset, 32); offset += 32
System.arraycopy(builderBytes, 0, encoded, offset, 32)
return keccak256(encoded)
}
@@ -10,7 +10,7 @@ import java.math.BigInteger
object EthereumUtils {
// Polymarket 合约地址(Polygon 主网)
private val COLLATERAL_TOKEN_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" // USDC
private val COLLATERAL_TOKEN_ADDRESS = "0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB" // pUSD
private val CONDITIONAL_TOKENS_ADDRESS = "0x4D97DCd97eC945f40cF65F87097ACe5EA0476045" // ConditionalTokens
/**
+10
View File
@@ -41,6 +41,7 @@ import { wsManager } from './services/websocket'
import type { OrderPushMessage } from './types'
import { apiService } from './services/api'
import { hasToken } from './utils'
import ClobMigrationModal, { STORAGE_KEY as CLOB_MIGRATION_KEY } from './components/ClobMigrationModal'
/**
*
@@ -64,6 +65,7 @@ function App() {
const { t, i18n } = useTranslation()
const [isFirstUse, setIsFirstUse] = useState<boolean | null>(null)
const [checking, setChecking] = useState(true)
const [clobMigrationVisible, setClobMigrationVisible] = useState(false)
// 根据当前语言设置 Ant Design 的 locale
const getAntdLocale = () => {
@@ -199,6 +201,13 @@ function App() {
wsManager.disconnect()
}
}, [checking, isFirstUse])
// 已登录且未查看过 CLOB V2 迁移提醒时显示弹窗
useEffect(() => {
if (!checking && isFirstUse === false && hasToken() && !localStorage.getItem(CLOB_MIGRATION_KEY)) {
setClobMigrationVisible(true)
}
}, [checking, isFirstUse])
// 订阅订单推送并显示全局通知
useEffect(() => {
@@ -284,6 +293,7 @@ function App() {
{/* 默认重定向到登录页 */}
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>
<ClobMigrationModal open={clobMigrationVisible} onClose={() => setClobMigrationVisible(false)} />
</BrowserRouter>
</ConfigProvider>
)
@@ -525,7 +525,7 @@ const AccountImportForm: React.FC<AccountImportFormProps> = ({
</Space>
{!option.error && (
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--ant-color-primary)' }}>
{formatUSDC(option.totalBalance)} USDC
${formatUSDC(option.totalBalance)}
</span>
)}
</div>
@@ -247,7 +247,7 @@ const AccountSetupStatusBlock: React.FC<AccountSetupStatusBlockProps> = ({
const displayText = isUnlimited
? t('accountSetup.approvalDetails.unlimited')
: isApproved
? `${parseFloat(allowance).toFixed(2)} USDC`
? `$${parseFloat(allowance).toFixed(2)}`
: t('accountSetup.approvalDetails.notApproved')
return (
<div
@@ -0,0 +1,64 @@
import { Modal, Button, Space } from 'antd'
import { SwapOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
const STORAGE_KEY = 'clob_v2_migration_dismissed'
interface ClobMigrationModalProps {
open: boolean
onClose: () => void
}
const ClobMigrationModal: React.FC<ClobMigrationModalProps> = ({ open, onClose }) => {
const { t } = useTranslation()
const navigate = useNavigate()
const handleClose = (dontRemind = false) => {
if (dontRemind) {
localStorage.setItem(STORAGE_KEY, 'true')
}
onClose()
}
const handleGoToAccounts = () => {
localStorage.setItem(STORAGE_KEY, 'true')
onClose()
navigate('/accounts')
}
return (
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<SwapOutlined style={{ color: '#fa8c16' }} />
<span>{t('clobMigration.title')}</span>
</div>
}
open={open}
onCancel={() => handleClose()}
footer={
<Space>
<Button onClick={() => handleClose(true)}>
{t('clobMigration.dontRemind')}
</Button>
<Button onClick={() => handleClose()}>
{t('common.later')}
</Button>
<Button type="primary" onClick={handleGoToAccounts}>
{t('clobMigration.goToAccounts')}
</Button>
</Space>
}
closable
maskClosable={false}
>
<p style={{ fontSize: 14, lineHeight: 1.8, color: 'rgba(0, 0, 0, 0.65)' }}>
{t('clobMigration.description')}
</p>
</Modal>
)
}
export default ClobMigrationModal
export { STORAGE_KEY }
+9
View File
@@ -1801,5 +1801,14 @@
"maxSizeUpdated": "Updated to max size",
"periodChanged": "Period has changed, popup closed"
}
},
"clobMigration": {
"title": "CLOB 2.0 Migration Notice",
"description": "Polymarket CLOB has been upgraded to V2. Please go to the Accounts page to complete the USDC migration to ensure trading functions properly.",
"goToAccounts": "Go to Accounts",
"dontRemind": "Don't remind again",
"accountGuide": "CLOB V2 requires pUSD for trading. Click the USDC.e → pUSD button in the account actions to complete the migration.",
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "Got it"
}
}
+9
View File
@@ -1801,5 +1801,14 @@
"maxSizeUpdated": "已更新为最大数量",
"periodChanged": "周期已切换,弹窗已关闭"
}
},
"clobMigration": {
"title": "CLOB 2.0 迁移提醒",
"description": "Polymarket CLOB 已升级至 V2 版本,您需要前往账户页完成 USDC 迁移操作,以确保交易功能正常使用。",
"goToAccounts": "前往账户页",
"dontRemind": "不再提醒",
"accountGuide": "CLOB V2 需要 pUSD 进行交易,请点击账户操作栏中的 USDC.e → pUSD 按钮完成迁移。",
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "知道了"
}
}
+9
View File
@@ -1801,5 +1801,14 @@
"maxSizeUpdated": "已更新為最大數量",
"periodChanged": "週期已切換,彈窗已關閉"
}
},
"clobMigration": {
"title": "CLOB 2.0 遷移提醒",
"description": "Polymarket CLOB 已升級至 V2 版本,您需要前往帳戶頁完成 USDC 遷移操作,以確保交易功能正常使用。",
"goToAccounts": "前往帳戶頁",
"dontRemind": "不再提醒",
"accountGuide": "CLOB V2 需要 pUSD 進行交易,請點擊帳戶操作欄中的 USDC.e → pUSD 按鈕完成遷移。",
"accountGuideButton": "USDC.e → pUSD",
"dismissGuide": "知道了"
}
}
+2 -2
View File
@@ -202,7 +202,7 @@ const AccountDetail: React.FC = () => {
<Spin size="small" />
) : balance ? (
<span style={{ fontWeight: 'bold', color: '#1890ff' }}>
{formatUSDC(balance)} USDC
${formatUSDC(balance)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -274,7 +274,7 @@ const AccountDetail: React.FC = () => {
fontWeight: 'bold',
color: account.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
}}>
{formatUSDC(account.totalPnl)} USDC
${formatUSDC(account.totalPnl)}
</span>
</Descriptions.Item>
)}
+116 -7
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert, Tooltip, List, Empty } from 'antd'
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined, EyeOutlined, DeleteOutlined, WalletOutlined } from '@ant-design/icons'
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined, EyeOutlined, DeleteOutlined, WalletOutlined, SwapOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { useAccountStore } from '../store/accountStore'
import type { Account } from '../types'
@@ -8,6 +8,7 @@ import { useMediaQuery } from 'react-responsive'
import { formatUSDC } from '../utils'
import AccountImportForm from '../components/AccountImportForm'
import AccountSetupStatusBlock from '../components/AccountSetupStatusBlock'
import apiService from '../services/api'
const { Title } = Typography
@@ -27,11 +28,57 @@ const AccountList: React.FC = () => {
const [editLoading, setEditLoading] = useState(false)
const [accountImportModalVisible, setAccountImportModalVisible] = useState(false)
const [accountImportForm] = Form.useForm()
const [wrapLoading, setWrapLoading] = useState<Record<number, boolean>>({})
const [migrationGuideVisible, setMigrationGuideVisible] = useState(false)
const ACCOUNT_GUIDE_KEY = 'clob_v2_account_guide_dismissed'
const handleWrapToPusd = async (account: Account) => {
try {
setWrapLoading(prev => ({ ...prev, [account.id]: true }))
const res = await apiService.accounts.getUsdceBalance(account.id)
if (res.data.code !== 0 || !res.data.data) {
message.error(res.data.msg || '查询 USDC.e 余额失败')
return
}
const balance = parseFloat(res.data.data.balance)
if (balance <= 0) {
message.info('USDC.e 余额为 0,无需迁移')
return
}
Modal.confirm({
title: 'USDC.e → pUSD 迁移',
content: `检测到 ${balance.toFixed(2)} USDC.e,将全部 wrap 为 pUSD。确认继续?`,
okText: '确认迁移',
cancelText: '取消',
onOk: async () => {
const wrapRes = await apiService.accounts.wrapToPusd(account.id)
if (wrapRes.data.code === 0) {
const txHash = wrapRes.data.data?.transactionHash
message.success(txHash ? `迁移成功,交易: ${txHash.slice(0, 10)}...` : '迁移成功(无需操作)')
fetchAccountBalance(account.id)
} else {
message.error(wrapRes.data.msg || '迁移失败')
}
}
})
} catch (e: any) {
message.error(e.message || '迁移失败')
} finally {
setWrapLoading(prev => ({ ...prev, [account.id]: false }))
}
}
useEffect(() => {
fetchAccounts()
}, [fetchAccounts])
// 首次进入且有账户时显示迁移引导
useEffect(() => {
if (!loading && accounts.length > 0 && !localStorage.getItem(ACCOUNT_GUIDE_KEY)) {
setMigrationGuideVisible(true)
}
}, [loading, accounts.length])
const handleAccountImportSuccess = async () => {
message.success(t('accountImport.importSuccess'))
setAccountImportModalVisible(false)
@@ -325,7 +372,7 @@ const AccountList: React.FC = () => {
}
const balanceObj = balanceMap[record.id]
const balance = balanceObj?.total || record.balance || '-'
return balance && balance !== '-' && typeof balance === 'string' ? `${formatUSDC(balance)} USDC` : '-'
return balance && balance !== '-' && typeof balance === 'string' ? `$${formatUSDC(balance)}` : '-'
}
},
{
@@ -374,6 +421,26 @@ const AccountList: React.FC = () => {
</div>
</Tooltip>
<Tooltip title="USDC.e → pUSD">
<div
onClick={() => handleWrapToPusd(record)}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '32px',
height: '32px',
cursor: wrapLoading[record.id] ? 'wait' : 'pointer',
borderRadius: '6px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#fff7e6'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = 'transparent'}
>
<SwapOutlined style={{ fontSize: '16px', color: '#fa8c16' }} spin={wrapLoading[record.id]} />
</div>
</Tooltip>
<Popconfirm
title={t('accountList.deleteConfirm')}
description={
@@ -438,6 +505,38 @@ const AccountList: React.FC = () => {
</Tooltip>
</div>
{migrationGuideVisible && !loading && accounts.length > 0 && (
<Alert
message={t('clobMigration.accountGuide')}
type="warning"
showIcon
icon={<SwapOutlined />}
closable
onClose={() => {
localStorage.setItem(ACCOUNT_GUIDE_KEY, 'true')
setMigrationGuideVisible(false)
}}
afterClose={() => {
localStorage.setItem(ACCOUNT_GUIDE_KEY, 'true')
setMigrationGuideVisible(false)
}}
style={{ marginBottom: 16, ...(isMobile ? { margin: '0 8px 12px' } : {}) }}
action={
<Button
size="small"
type="primary"
danger
onClick={() => {
localStorage.setItem(ACCOUNT_GUIDE_KEY, 'true')
setMigrationGuideVisible(false)
}}
>
{t('clobMigration.dismissGuide')}
</Button>
}
/>
)}
<Card style={{
margin: isMobile ? '0 -8px' : '0',
borderRadius: isMobile ? '0' : undefined
@@ -504,7 +603,7 @@ const AccountList: React.FC = () => {
{t('accountList.totalBalance')}
</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#52c41a' }}>
{balance?.total && balance.total !== '-' ? `${formatUSDC(balance.total)} USDC` : '- USDC'}
{balance?.total && balance.total !== '-' ? `$${formatUSDC(balance.total)}` : '-'}
</div>
</div>
<div style={{ textAlign: 'right' }}>
@@ -568,6 +667,16 @@ const AccountList: React.FC = () => {
</div>
</Tooltip>
<Tooltip title={t('clobMigration.accountGuideButton')}>
<div
onClick={() => handleWrapToPusd(account)}
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', cursor: wrapLoading[account.id] ? 'wait' : 'pointer', padding: '4px 8px' }}
>
<SwapOutlined style={{ fontSize: '18px', color: '#fa8c16' }} spin={wrapLoading[account.id]} />
<span style={{ fontSize: '10px', color: '#8c8c8c', marginTop: '2px' }}>{t('clobMigration.accountGuideButton')}</span>
</div>
</Tooltip>
<Popconfirm
title={t('accountList.deleteConfirm')}
description={
@@ -728,7 +837,7 @@ const AccountList: React.FC = () => {
<Spin size="small" />
) : detailBalance ? (
<span style={{ fontWeight: 'bold', color: '#1890ff', fontSize: '16px' }}>
{formatUSDC(detailBalance.total)} USDC
${formatUSDC(detailBalance.total)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -739,7 +848,7 @@ const AccountList: React.FC = () => {
<Spin size="small" />
) : detailBalance ? (
<span style={{ color: '#52c41a' }}>
{formatUSDC(detailBalance.available)} USDC
${formatUSDC(detailBalance.available)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -750,7 +859,7 @@ const AccountList: React.FC = () => {
<Spin size="small" />
) : detailBalance ? (
<span style={{ color: '#1890ff' }}>
{formatUSDC(detailBalance.position)} USDC
${formatUSDC(detailBalance.position)}
</span>
) : (
<span style={{ color: '#999' }}>-</span>
@@ -806,7 +915,7 @@ const AccountList: React.FC = () => {
fontWeight: 'bold',
color: detailAccount.totalPnl && detailAccount.totalPnl.startsWith('-') ? '#ff4d4f' : '#52c41a'
}}>
{formatUSDC(detailAccount.totalPnl)} USDC
${formatUSDC(detailAccount.totalPnl)}
</span>
</Descriptions.Item>
)}
+5 -3
View File
@@ -10,6 +10,8 @@ interface BacktestChartProps {
}[]
}
// Bug #39 Note: This chart currently displays cash balance (balanceAfter), not total equity.
// A true equity curve (cash + position value) would require an equityAfter field in the trade records.
const BacktestChart: React.FC<BacktestChartProps> = ({ trades }) => {
const { t } = useTranslation()
const chartRef = useRef<HTMLDivElement>(null)
@@ -73,9 +75,9 @@ const BacktestChart: React.FC<BacktestChartProps> = ({ trades }) => {
return `
<div>
<div>${t('backtest.tradeTime')}: ${param.name}</div>
<div>${t('backtest.balanceAfter')}: ${value} USDC</div>
<div>${t('backtest.balanceAfter')}: $${value}</div>
<div style="color: ${color}">
${t('backtest.profitLoss')}: ${diff} USDC (${diffPercent}%)
${t('backtest.profitLoss')}: $${diff} (${diffPercent}%)
</div>
</div>
`
@@ -119,7 +121,7 @@ const BacktestChart: React.FC<BacktestChartProps> = ({ trades }) => {
},
yAxis: {
type: 'value',
name: 'USDC',
name: '$',
nameLocation: 'end',
nameGap: 10,
axisLabel: {
+5 -5
View File
@@ -279,14 +279,14 @@ const BacktestDetail: React.FC = () => {
render: (value: string) => parseFloat(value).toFixed(4)
},
{
title: t('backtest.amount') + ' (USDC)',
title: t('backtest.amount') + ' ($)',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (value: string) => formatUSDC(value)
},
{
title: t('backtest.balanceAfter') + ' (USDC)',
title: t('backtest.balanceAfter') + ' ($)',
dataIndex: 'balanceAfter',
key: 'balanceAfter',
width: 120,
@@ -346,14 +346,14 @@ const BacktestDetail: React.FC = () => {
{task.leaderName || task.leaderAddress}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.initialBalance')}>
{formatUSDC(task.initialBalance)} USDC
${formatUSDC(task.initialBalance)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.finalBalance')}>
{task.finalBalance ? formatUSDC(task.finalBalance) + ' USDC' : '-'}
{task.finalBalance ? '$' + formatUSDC(task.finalBalance) : '-'}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitAmount')}>
<span style={{ color: task.profitAmount && parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>
{task.profitAmount ? formatUSDC(task.profitAmount) + ' USDC' : '-'}
{task.profitAmount ? '$' + formatUSDC(task.profitAmount) : '-'}
</span>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitRate')}>
+17 -17
View File
@@ -461,14 +461,14 @@ const BacktestList: React.FC = () => {
render: (value: string) => parseFloat(value).toFixed(4)
},
{
title: t('backtest.amount') + ' (USDC)',
title: t('backtest.amount') + ' ($)',
dataIndex: 'amount',
key: 'amount',
width: 120,
render: (value: string) => formatUSDC(value)
},
{
title: t('backtest.balanceAfter') + ' (USDC)',
title: t('backtest.balanceAfter') + ' ($)',
dataIndex: 'balanceAfter',
key: 'balanceAfter',
width: 120,
@@ -815,7 +815,7 @@ const BacktestList: React.FC = () => {
<div>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('backtest.profitAmount')}</div>
{task.profitAmount != null ? (
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>{formatUSDC(task.profitAmount)} USDC</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: parseFloat(task.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>${formatUSDC(task.profitAmount)}</div>
) : (
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
)}
@@ -997,7 +997,7 @@ const BacktestList: React.FC = () => {
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.initialBalance') + ' (USDC)'}
label={t('backtest.initialBalance') + ' ($)'}
name="initialBalance"
rules={[
{ required: true, message: t('backtest.initialBalanceRequired') || '请输入初始资金' },
@@ -1082,7 +1082,7 @@ const BacktestList: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label={t('backtest.fixedAmount') + ' (USDC)'}
label={t('backtest.fixedAmount') + ' ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('backtest.fixedAmountRequired') || '请输入固定金额' },
@@ -1101,7 +1101,7 @@ const BacktestList: React.FC = () => {
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxOrderSize') + ' (USDC)'}
label={t('backtest.maxOrderSize') + ' ($)'}
name="maxOrderSize"
rules={[{ required: true }]}
>
@@ -1110,7 +1110,7 @@ const BacktestList: React.FC = () => {
</Col>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.minOrderSize') + ' (USDC)'}
label={t('backtest.minOrderSize') + ' ($)'}
name="minOrderSize"
rules={[{ required: true }]}
>
@@ -1122,7 +1122,7 @@ const BacktestList: React.FC = () => {
<Row gutter={24}>
<Col xs={24} sm={24} md={12}>
<Form.Item
label={t('backtest.maxDailyLoss') + ' (USDC)'}
label={t('backtest.maxDailyLoss') + ' ($)'}
name="maxDailyLoss"
rules={[{ required: true }]}
>
@@ -1141,7 +1141,7 @@ const BacktestList: React.FC = () => {
</Row>
<Form.Item
label={t('backtest.maxPositionValue') + ' (USDC)'}
label={t('backtest.maxPositionValue') + ' ($)'}
name="maxPositionValue"
>
<InputNumber
@@ -1316,14 +1316,14 @@ const BacktestList: React.FC = () => {
{detailTask.leaderName || `Leader ${detailTask.leaderId}`}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.initialBalance')}>
{formatUSDC(detailTask.initialBalance)} USDC
${formatUSDC(detailTask.initialBalance)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.finalBalance')}>
{detailTask.finalBalance ? formatUSDC(detailTask.finalBalance) + ' USDC' : '-'}
{detailTask.finalBalance ? '$' + formatUSDC(detailTask.finalBalance) : '-'}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitAmount')}>
<span style={{ color: detailTask.profitAmount && parseFloat(detailTask.profitAmount) >= 0 ? '#52c41a' : '#ff4d4f' }}>
{detailTask.profitAmount ? formatUSDC(detailTask.profitAmount) + ' USDC' : '-'}
{detailTask.profitAmount ? '$' + formatUSDC(detailTask.profitAmount) : '-'}
</span>
</Descriptions.Item>
<Descriptions.Item label={t('backtest.profitRate')}>
@@ -1439,17 +1439,17 @@ const BacktestList: React.FC = () => {
<Descriptions.Item label={t('backtest.copyMode')}>
{detailConfig.copyMode === 'RATIO'
? `${t('backtest.copyModeRatio')} ${parseFloat(detailConfig.copyRatio) * 100}%`
: `${t('backtest.copyModeFixed')} ${formatUSDC(detailConfig.fixedAmount)} USDC`
: `${t('backtest.copyModeFixed')} $${formatUSDC(detailConfig.fixedAmount)}`
}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxOrderSize')}>
{formatUSDC(detailConfig.maxOrderSize)} USDC
{'$' + formatUSDC(detailConfig.maxOrderSize)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.minOrderSize')}>
{formatUSDC(detailConfig.minOrderSize)} USDC
{'$' + formatUSDC(detailConfig.minOrderSize)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxDailyLoss')}>
{formatUSDC(detailConfig.maxDailyLoss)} USDC
{'$' + formatUSDC(detailConfig.maxDailyLoss)}
</Descriptions.Item>
<Descriptions.Item label={t('backtest.maxDailyOrders')}>
{detailConfig.maxDailyOrders}
@@ -1471,7 +1471,7 @@ const BacktestList: React.FC = () => {
)}
{detailConfig.maxPositionValue && (
<Descriptions.Item label={t('backtest.maxPositionValue')}>
{formatUSDC(detailConfig.maxPositionValue)} USDC
{'$' + formatUSDC(detailConfig.maxPositionValue)}
</Descriptions.Item>
)}
{(detailConfig.minPrice || detailConfig.maxPrice) && (
+2 -2
View File
@@ -150,7 +150,7 @@ const CopyTradingBuyOrdersPage: React.FC = () => {
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -305,7 +305,7 @@ const CopyTradingBuyOrdersPage: React.FC = () => {
: {formatUSDC(order.quantity)} | : {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
: {formatUSDC(amount)} USDC
金额: ${formatUSDC(amount)}
</div>
</div>
+3 -3
View File
@@ -279,7 +279,7 @@ const CopyTradingList: React.FC = () => {
fontSize: isMobile ? 12 : 14
}}>
{getPnlIcon(stats.totalPnl)}
{isMobile ? formatUSDC(stats.totalPnl) : `${formatUSDC(stats.totalPnl)} USDC`}
{isMobile ? formatUSDC(stats.totalPnl) : `$${formatUSDC(stats.totalPnl)}`}
</div>
{!isMobile && (
<div style={{
@@ -520,7 +520,7 @@ const CopyTradingList: React.FC = () => {
<div style={{ fontSize: '12px', opacity: '0.9' }}>
{record.copyMode === 'RATIO'
? `${t('copyTradingList.ratioMode') || '比例'} ${(parseFloat(record.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
: `${t('copyTradingList.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
: `${t('copyTradingList.fixedAmountMode') || '固定'} $${formatUSDC(record.fixedAmount || '0')}`
}
</div>
</div>
@@ -549,7 +549,7 @@ const CopyTradingList: React.FC = () => {
gap: '4px'
}}>
{getPnlIcon(stats.totalPnl)}
{formatUSDC(stats.totalPnl)} USDC
${formatUSDC(stats.totalPnl)}
</div>
) : loadingStatistics.has(record.id) ? (
<Spin size="small" />
@@ -130,7 +130,7 @@ const CopyTradingMatchedOrdersPage: React.FC = () => {
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -262,7 +262,7 @@ const CopyTradingMatchedOrdersPage: React.FC = () => {
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
@@ -518,7 +518,7 @@ const AddModal: React.FC<AddModalProps> = ({
value={parseFloat(leaderAssetInfo.total)}
precision={4}
valueStyle={{ color: '#52c41a', fontWeight: 'bold', fontSize: '16px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -528,7 +528,7 @@ const AddModal: React.FC<AddModalProps> = ({
value={parseFloat(leaderAssetInfo.available)}
precision={4}
valueStyle={{ color: '#1890ff', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -538,7 +538,7 @@ const AddModal: React.FC<AddModalProps> = ({
value={parseFloat(leaderAssetInfo.position)}
precision={4}
valueStyle={{ color: '#722ed1', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -606,7 +606,7 @@ const AddModal: React.FC<AddModalProps> = ({
{copyMode === 'FIXED' && (
<Form.Item
label={t('copyTradingAdd.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('copyTradingAdd.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('copyTradingAdd.fixedAmountRequired') || '请输入固定跟单金额' },
@@ -645,7 +645,7 @@ const AddModal: React.FC<AddModalProps> = ({
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('copyTradingAdd.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('copyTradingAdd.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('copyTradingAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
>
@@ -665,7 +665,7 @@ const AddModal: React.FC<AddModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingAdd.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('copyTradingAdd.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('copyTradingAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
rules={[
@@ -700,7 +700,7 @@ const AddModal: React.FC<AddModalProps> = ({
)}
<Form.Item
label={t('copyTradingAdd.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
label={t('copyTradingAdd.maxDailyLoss') || '每日最大亏损限制 ($)'}
name="maxDailyLoss"
tooltip={t('copyTradingAdd.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
>
@@ -709,7 +709,7 @@ const AddModal: React.FC<AddModalProps> = ({
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder={t('copyTradingAdd.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
placeholder={t('copyTradingAdd.maxDailyLossPlaceholder') || '默认 10000 $(可选)'}
formatter={(value) => {
if (!value && value !== 0) return ''
const num = parseFloat(value.toString())
@@ -767,7 +767,7 @@ const AddModal: React.FC<AddModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingAdd.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('copyTradingAdd.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('copyTradingAdd.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
@@ -853,7 +853,7 @@ const AddModal: React.FC<AddModalProps> = ({
<Divider>{t('copyTradingAdd.positionLimitFilter') || '最大仓位限制'}</Divider>
<Form.Item
label={t('copyTradingAdd.maxPositionValue') || '最大仓位金额 (USDC)'}
label={t('copyTradingAdd.maxPositionValue') || '最大仓位金额 ($)'}
name="maxPositionValue"
tooltip={t('copyTradingAdd.maxPositionValueTooltip') || '限制单个市场的最大仓位金额。如果该市场的当前仓位金额 + 跟单金额超过此限制,则不会下单。不填写则不启用此限制'}
>
@@ -1082,7 +1082,7 @@ const AddModal: React.FC<AddModalProps> = ({
<span>
{record.copyMode === 'RATIO'
? `${t('copyTradingAdd.ratioMode') || '比例'} ${record.copyRatio}x`
: `${t('copyTradingAdd.fixedAmountMode') || '固定'} ${formatUSDC(record.fixedAmount || '0')} USDC`
: `${t('copyTradingAdd.fixedAmountMode') || '固定'} $${formatUSDC(record.fixedAmount || '0')}`
}
</span>
)
@@ -261,7 +261,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -384,7 +384,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
</div>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', fontSize: isMobile ? '12px' : '13px', color: '#666' }}>
<span>{t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count}</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: ${formatUSDC(group.stats.totalAmount)}</span>
<span>
{t('copyTradingOrders.statusBreakdown') || '状态'}:
{group.stats.fullyMatchedCount > 0 && ` ${t('copyTradingOrders.allFullySold') || '全部卖出'} ${group.stats.fullyMatchedCount}`}
@@ -460,7 +460,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
<div style={{ fontSize: '12px', color: '#666' }}>
<div>{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}</div>
<div>{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC</div>
<div>{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}</div>
<div>{t('copyTradingOrders.matched') || '已匹配'}: {formatUSDC(order.matchedQuantity)} | {t('copyTradingOrders.remaining') || '剩余'}: {formatUSDC(order.remainingQuantity)}</div>
<div style={{ color: '#999', marginTop: '4px' }}>{formattedDate}</div>
</div>
@@ -557,7 +557,7 @@ const BuyOrdersTab: React.FC<BuyOrdersTabProps> = ({ copyTradingId, active = fal
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}
</div>
</div>
@@ -356,7 +356,7 @@ const EditModal: React.FC<EditModalProps> = ({
value={parseFloat(leaderAssetInfo.total)}
precision={4}
valueStyle={{ color: '#52c41a', fontWeight: 'bold', fontSize: '16px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -366,7 +366,7 @@ const EditModal: React.FC<EditModalProps> = ({
value={parseFloat(leaderAssetInfo.available)}
precision={4}
valueStyle={{ color: '#1890ff', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -376,7 +376,7 @@ const EditModal: React.FC<EditModalProps> = ({
value={parseFloat(leaderAssetInfo.position)}
precision={4}
valueStyle={{ color: '#722ed1', fontSize: '14px' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Col>
@@ -456,7 +456,7 @@ const EditModal: React.FC<EditModalProps> = ({
{copyMode === 'FIXED' && (
<Form.Item
label={t('copyTradingEdit.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('copyTradingEdit.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('copyTradingEdit.fixedAmountRequired') || '请输入固定跟单金额' },
@@ -495,7 +495,7 @@ const EditModal: React.FC<EditModalProps> = ({
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('copyTradingEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('copyTradingEdit.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('copyTradingEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限'}
>
@@ -515,7 +515,7 @@ const EditModal: React.FC<EditModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('copyTradingEdit.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('copyTradingEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,必须 >= 1'}
rules={[
@@ -550,7 +550,7 @@ const EditModal: React.FC<EditModalProps> = ({
)}
<Form.Item
label={t('copyTradingEdit.maxDailyLoss') || '每日最大亏损限制 (USDC)'}
label={t('copyTradingEdit.maxDailyLoss') || '每日最大亏损限制 ($)'}
name="maxDailyLoss"
tooltip={t('copyTradingEdit.maxDailyLossTooltip') || '限制每日最大亏损金额,用于风险控制'}
>
@@ -559,7 +559,7 @@ const EditModal: React.FC<EditModalProps> = ({
step={0.0001}
precision={4}
style={{ width: '100%' }}
placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 USDC(可选)'}
placeholder={t('copyTradingEdit.maxDailyLossPlaceholder') || '默认 10000 $(可选)'}
formatter={(value) => {
if (!value && value !== 0) return ''
const num = parseFloat(value.toString())
@@ -617,7 +617,7 @@ const EditModal: React.FC<EditModalProps> = ({
</Form.Item>
<Form.Item
label={t('copyTradingEdit.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('copyTradingEdit.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('copyTradingEdit.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
@@ -703,7 +703,7 @@ const EditModal: React.FC<EditModalProps> = ({
<Divider>{t('copyTradingEdit.positionLimitFilter') || '最大仓位限制'}</Divider>
<Form.Item
label={t('copyTradingEdit.maxPositionValue') || '最大仓位金额 (USDC)'}
label={t('copyTradingEdit.maxPositionValue') || '最大仓位金额 ($)'}
name="maxPositionValue"
tooltip={t('copyTradingEdit.maxPositionValueTooltip') || '限制单个市场的最大仓位金额。如果该市场的当前仓位金额 + 跟单金额超过此限制,则不会下单。不填写则不启用此限制'}
>
@@ -228,7 +228,7 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -408,7 +408,7 @@ const MatchedOrdersTab: React.FC<MatchedOrdersTabProps> = ({ copyTradingId, acti
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
@@ -258,7 +258,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -274,7 +274,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -362,10 +362,10 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
</div>
<div style={{ display: 'flex', gap: '16px', flexWrap: 'wrap', fontSize: isMobile ? '12px' : '13px', color: '#666' }}>
<span>{t('copyTradingOrders.orderCount') || '订单数'}: {group.stats.count}</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: {formatUSDC(group.stats.totalAmount)} USDC</span>
<span>{t('copyTradingOrders.totalAmount') || '总金额'}: ${formatUSDC(group.stats.totalAmount)}</span>
{group.stats.totalPnl && (
<span style={{ color: pnlColor, fontWeight: 500 }}>
{t('copyTradingOrders.totalPnl') || '总盈亏'}: {formatUSDC(group.stats.totalPnl)} USDC
{t('copyTradingOrders.totalPnl') || '总盈亏'}: ${formatUSDC(group.stats.totalPnl)}
</span>
)}
</div>
@@ -434,9 +434,9 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
<div style={{ fontSize: '12px', color: '#666' }}>
<div>{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}</div>
<div>{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC</div>
<div>{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}</div>
<div style={{ color: getPnlColor(order.realizedPnl), fontWeight: 500 }}>
{t('copyTradingOrders.realizedPnl') || '已实现盈亏'}: {formatUSDC(order.realizedPnl)} USDC
{t('copyTradingOrders.realizedPnl') || '已实现盈亏'}: ${formatUSDC(order.realizedPnl)}
</div>
<div style={{ color: '#999', marginTop: '4px' }}>{formattedDate}</div>
</div>
@@ -530,7 +530,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
{t('copyTradingOrders.quantity') || '数量'}: {formatUSDC(order.quantity)} | {t('copyTradingOrders.price') || '价格'}: {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
{t('copyTradingOrders.amount') || '金额'}: {formatUSDC(amount)} USDC
{t('copyTradingOrders.amount') || '金额'}: ${formatUSDC(amount)}
</div>
</div>
@@ -541,7 +541,7 @@ const SellOrdersTab: React.FC<SellOrdersTabProps> = ({ copyTradingId, active = f
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
@@ -104,7 +104,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
<ArrowUpOutlined style={{ color: '#1890ff', fontSize: '14px' }} />
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalBuyAmount)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalBuyAmount)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -113,7 +113,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: '#333', flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
<ArrowDownOutlined style={{ color: '#ff4d4f', fontSize: '14px' }} />
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalSellAmount)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalSellAmount)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -122,7 +122,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: getPnlColor(statistics.totalPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
{getPnlIcon(statistics.totalPnl)}
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalPnl)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalPnl)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -131,7 +131,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.totalRealizedPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
{getPnlIcon(statistics.totalRealizedPnl)}
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalRealizedPnl)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalRealizedPnl)}</span>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px', borderBottom: '1px solid #f0f0f0' }}>
@@ -140,7 +140,7 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
</div>
<div style={{ fontSize: '16px', fontWeight: '500', color: getPnlColor(statistics.totalUnrealizedPnl), flex: '1', textAlign: 'right', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '4px' }}>
{getPnlIcon(statistics.totalUnrealizedPnl)}
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>{formatUSDC(statistics.totalUnrealizedPnl)} USDC</span>
<span style={{ fontSize: 'clamp(12px, 4vw, 16px)' }}>${formatUSDC(statistics.totalUnrealizedPnl)}</span>
</div>
</div>
</div>
@@ -165,43 +165,38 @@ const StatisticsModal: React.FC<StatisticsModalProps> = ({
<Statistic
title={t('copyTradingOrders.totalBuyAmount') || '总买入金额'}
value={formatUSDC(statistics.totalBuyAmount)}
suffix="USDC"
prefix={<ArrowUpOutlined style={{ color: '#1890ff' }} />}
prefix={<><ArrowUpOutlined style={{ color: '#1890ff' }} /> $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalSellAmount') || '总卖出金额'}
value={formatUSDC(statistics.totalSellAmount)}
suffix="USDC"
prefix={<ArrowDownOutlined style={{ color: '#ff4d4f' }} />}
prefix={<><ArrowDownOutlined style={{ color: '#ff4d4f' }} /> $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalPnl') || '总盈亏'}
value={formatUSDC(statistics.totalPnl)}
suffix="USDC"
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
prefix={getPnlIcon(statistics.totalPnl)}
prefix={<>{getPnlIcon(statistics.totalPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalRealizedPnl') || '总已实现盈亏'}
value={formatUSDC(statistics.totalRealizedPnl)}
suffix="USDC"
valueStyle={{ color: getPnlColor(statistics.totalRealizedPnl) }}
prefix={getPnlIcon(statistics.totalRealizedPnl)}
prefix={<>{getPnlIcon(statistics.totalRealizedPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={8}>
<Statistic
title={t('copyTradingOrders.totalUnrealizedPnl') || '总未实现盈亏'}
value={formatUSDC(statistics.totalUnrealizedPnl)}
suffix="USDC"
valueStyle={{ color: getPnlColor(statistics.totalUnrealizedPnl) }}
prefix={getPnlIcon(statistics.totalUnrealizedPnl)}
prefix={<>{getPnlIcon(statistics.totalUnrealizedPnl)} $</>}
/>
</Col>
</Row>
+4 -4
View File
@@ -145,7 +145,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
const amount = (parseFloat(record.quantity) * parseFloat(record.price)).toString()
return (
<span style={{ fontSize: isMobile ? 12 : 14 }}>
{isMobile ? formatUSDC(amount) : `${formatUSDC(amount)} USDC`}
{isMobile ? formatUSDC(amount) : `$${formatUSDC(amount)}`}
</span>
)
}
@@ -161,7 +161,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
fontWeight: 500,
fontSize: isMobile ? 12 : 14
}}>
{isMobile ? formatUSDC(value) : `${formatUSDC(value)} USDC`}
{isMobile ? formatUSDC(value) : `$${formatUSDC(value)}`}
</span>
)
},
@@ -277,7 +277,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
: {formatUSDC(order.quantity)} | : {formatUSDC(order.price)}
</div>
<div style={{ fontSize: '14px', fontWeight: '500', marginTop: '4px' }}>
: {formatUSDC(amount)} USDC
金额: ${formatUSDC(amount)}
</div>
</div>
@@ -289,7 +289,7 @@ const CopyTradingSellOrdersPage: React.FC = () => {
fontWeight: 'bold',
color: getPnlColor(order.realizedPnl)
}}>
{formatUSDC(order.realizedPnl)} USDC
${formatUSDC(order.realizedPnl)}
</div>
</div>
+5 -8
View File
@@ -145,7 +145,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
<Statistic
title="总买入金额"
value={formatUSDC(statistics.totalBuyAmount)}
suffix="USDC"
prefix="$"
/>
</Col>
<Col xs={24} sm={12} md={6}>
@@ -179,7 +179,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
<Statistic
title="总卖出金额"
value={formatUSDC(statistics.totalSellAmount)}
suffix="USDC"
prefix="$"
/>
</Col>
<Col xs={24} sm={12} md={8}>
@@ -220,8 +220,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
title="总已实现盈亏"
value={formatUSDC(statistics.totalRealizedPnl)}
valueStyle={{ color: getPnlColor(statistics.totalRealizedPnl) }}
prefix={getPnlIcon(statistics.totalRealizedPnl)}
suffix="USDC"
prefix={<>{getPnlIcon(statistics.totalRealizedPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={6}>
@@ -229,8 +228,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
title="总未实现盈亏"
value={formatUSDC(statistics.totalUnrealizedPnl)}
valueStyle={{ color: getPnlColor(statistics.totalUnrealizedPnl) }}
prefix={getPnlIcon(statistics.totalUnrealizedPnl)}
suffix="USDC"
prefix={<>{getPnlIcon(statistics.totalUnrealizedPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={6}>
@@ -238,8 +236,7 @@ const CopyTradingStatisticsPage: React.FC = () => {
title="总盈亏"
value={formatUSDC(statistics.totalPnl)}
valueStyle={{ color: getPnlColor(statistics.totalPnl) }}
prefix={getPnlIcon(statistics.totalPnl)}
suffix="USDC"
prefix={<>{getPnlIcon(statistics.totalPnl)} $</>}
/>
</Col>
<Col xs={24} sm={12} md={6}>
+1 -1
View File
@@ -493,7 +493,7 @@ const CryptoTailMonitor: React.FC = () => {
} else {
timeStr = '--'
}
return `<span style="font-size:12px">${timeStr} &nbsp; ${Number(val).toFixed(2)} USDC</span>`
return `<span style="font-size:12px">${timeStr} &nbsp; $${Number(val).toFixed(2)}</span>`
}
},
legend: {
@@ -45,12 +45,12 @@ const CryptoTailPnlCurveModal: React.FC<CryptoTailPnlCurveModalProps> = (props)
if (!v) return ''
const d = data.curveData.find((p) => p.timestamp === v[0])
if (!d) return ''
return dayjs(v[0]).format('YYYY-MM-DD HH:mm') + '<br/>' + t('cryptoTailStrategy.pnlCurve.totalPnl') + ': ' + formatUSDC(d.cumulativePnl) + ' USDC'
return dayjs(v[0]).format('YYYY-MM-DD HH:mm') + '<br/>' + t('cryptoTailStrategy.pnlCurve.totalPnl') + ': $' + formatUSDC(d.cumulativePnl)
}
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true },
xAxis: { type: 'time' },
yAxis: { type: 'value', axisLabel: { formatter: (val: number) => String(val) + ' USDC' } },
yAxis: { type: 'value', axisLabel: { formatter: (val: number) => '$' + String(val) } },
series: [{
name: t('cryptoTailStrategy.pnlCurve.totalPnl'),
type: 'line',
@@ -108,7 +108,7 @@ const CryptoTailPnlCurveModal: React.FC<CryptoTailPnlCurveModalProps> = (props)
<Statistic
title={t('cryptoTailStrategy.pnlCurve.totalPnl')}
value={data?.totalRealizedPnl != null ? formatUSDC(data.totalRealizedPnl) : '-'}
suffix="USDC"
prefix="$"
valueStyle={{ color: pnlColor(data?.totalRealizedPnl ?? null) }}
/>
</Col>
@@ -125,7 +125,7 @@ const CryptoTailPnlCurveModal: React.FC<CryptoTailPnlCurveModalProps> = (props)
<Statistic
title={t('cryptoTailStrategy.pnlCurve.maxDrawdown')}
value={data?.maxDrawdown != null ? '-' + formatUSDC(data.maxDrawdown) : '-'}
suffix="USDC"
prefix="$"
valueStyle={{ color: data?.maxDrawdown ? '#ff4d4f' : undefined }}
/>
</Col>
@@ -773,7 +773,7 @@ const CryptoTailStrategyList: React.FC = () => {
<div>
<div style={{ fontSize: '10px', color: '#8c8c8c' }}>{t('cryptoTailStrategy.list.totalRealizedPnl')}</div>
{item.totalRealizedPnl != null ? (
<div style={{ fontSize: '14px', fontWeight: '600', color: pnlColor(item.totalRealizedPnl) }}>{formatUSDC(item.totalRealizedPnl)} USDC</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: pnlColor(item.totalRealizedPnl) }}>${formatUSDC(item.totalRealizedPnl)}</div>
) : (
<div style={{ fontSize: '14px', color: '#8c8c8c' }}>-</div>
)}
@@ -966,7 +966,7 @@ const CryptoTailStrategyList: React.FC = () => {
</Form.Item>
) : (
<Form.Item name="amountValue" label={t('cryptoTailStrategy.form.fixedUsdc')} rules={[{ required: true }]}>
<InputNumber min={1} style={{ width: '100%' }} addonAfter="USDC" stringMode />
<InputNumber min={1} style={{ width: '100%' }} addonBefore="$" stringMode />
</Form.Item>
)
}
@@ -1111,7 +1111,7 @@ const CryptoTailStrategyList: React.FC = () => {
dataIndex: 'amountUsdc',
key: 'amountUsdc',
width: 110,
render: (v: string) => `${formatUSDC(v)} USDC`
render: (v: string) => `$${formatUSDC(v)}`
},
{
title: t('cryptoTailStrategy.triggerRecords.realizedPnl'),
@@ -1186,7 +1186,7 @@ const CryptoTailStrategyList: React.FC = () => {
dataIndex: 'amountUsdc',
key: 'amountUsdc',
width: 110,
render: (v: string) => `${formatUSDC(v)} USDC`
render: (v: string) => `$${formatUSDC(v)}`
},
{
title: t('cryptoTailStrategy.triggerRecords.failReason'),
+5 -5
View File
@@ -254,7 +254,7 @@ const LeaderList: React.FC = () => {
return (
<Space direction="vertical" size={0}>
<Text style={{ color: '#52c41a', fontSize: '14px', fontWeight: '500' }}>
{balance.available === '-' ? '-' : `${formatUSDC(balance.available)} USDC`}
{balance.available === '-' ? '-' : `$${formatUSDC(balance.available)}`}
</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>
{t('leaderDetail.positionBalance')}: {formatUSDC(balance.position)}
@@ -488,7 +488,7 @@ const LeaderList: React.FC = () => {
{t('leaderDetail.availableBalance')}
</div>
<div style={{ fontSize: '14px', fontWeight: '600', color: '#52c41a' }}>
{balance?.available && balance.available !== '-' ? `${formatUSDC(balance.available)} USDC` : '- USDC'}
{balance?.available && balance.available !== '-' ? `$${formatUSDC(balance.available)}` : '-'}
</div>
</div>
<div style={{ textAlign: 'right' }}>
@@ -700,7 +700,7 @@ const LeaderList: React.FC = () => {
value={parseFloat(detailBalance.availableBalance)}
precision={4}
valueStyle={{ color: '#1890ff' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Card>
@@ -712,7 +712,7 @@ const LeaderList: React.FC = () => {
value={parseFloat(detailBalance.positionBalance)}
precision={4}
valueStyle={{ color: '#722ed1' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Card>
@@ -724,7 +724,7 @@ const LeaderList: React.FC = () => {
value={parseFloat(detailBalance.totalBalance)}
precision={4}
valueStyle={{ color: '#52c41a', fontWeight: 'bold' }}
suffix="USDC"
prefix="$"
formatter={(value) => formatUSDC(value?.toString() || '0')}
/>
</Card>
+1 -1
View File
@@ -118,7 +118,7 @@ const OrderList: React.FC = () => {
key: 'pnl',
render: (pnl: string | undefined) => pnl ? (
<span style={{ color: pnl.startsWith('-') ? 'red' : 'green' }}>
{formatUSDC(pnl)} USDC
${formatUSDC(pnl)}
</span>
) : '-'
},
+15 -15
View File
@@ -695,7 +695,7 @@ const PositionList: React.FC = () => {
fontWeight: '500',
color: isProfit ? '#52c41a' : '#f5222d'
}}>
{pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
{pnlNum >= 0 ? '+' : ''}${formatUSDC(position.pnl)}
</span>
</div>
)}
@@ -718,7 +718,7 @@ const PositionList: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '500' }}>
{formatUSDC(position.initialValue)} USDC
${formatUSDC(position.initialValue)}
</span>
</div>
{positionFilter === 'current' && position.currentPrice && (
@@ -732,7 +732,7 @@ const PositionList: React.FC = () => {
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#666' }}></span>
<span style={{ fontSize: '13px', fontWeight: '600' }}>
{formatUSDC(position.currentValue)} USDC
${formatUSDC(position.currentValue)}
</span>
</div>
</>
@@ -775,7 +775,7 @@ const PositionList: React.FC = () => {
fontWeight: 'bold',
color: isProfit ? '#52c41a' : '#f5222d'
}}>
{pnlNum >= 0 ? '+' : ''}{formatUSDC(position.pnl)} USDC
{pnlNum >= 0 ? '+' : ''}${formatUSDC(position.pnl)}
</span>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
@@ -802,7 +802,7 @@ const PositionList: React.FC = () => {
color: parseFloat(position.realizedPnl) >= 0 ? '#52c41a' : '#f5222d',
fontWeight: '500'
}}>
{parseFloat(position.realizedPnl) >= 0 ? '+' : ''}{formatUSDC(position.realizedPnl)} USDC
{parseFloat(position.realizedPnl) >= 0 ? '+' : ''}${formatUSDC(position.realizedPnl)}
</span>
</div>
)}
@@ -951,7 +951,7 @@ const PositionList: React.FC = () => {
dataIndex: 'initialValue',
key: 'initialValue',
render: (value: string) => (
<span>{formatUSDC(value)} USDC</span>
<span>${formatUSDC(value)}</span>
),
align: 'right' as const,
width: 110
@@ -971,7 +971,7 @@ const PositionList: React.FC = () => {
return (
<div>
<div style={{ fontWeight: '600', marginBottom: '2px' }}>
{formatUSDC(record.currentValue)} USDC
${formatUSDC(record.currentValue)}
</div>
<div style={{
fontSize: '13px',
@@ -1215,7 +1215,7 @@ const PositionList: React.FC = () => {
borderColor: '#52c41a'
}}
>
({redeemableSummary.totalCount}, {formatUSDC(redeemableSummary.totalValue)} USDC)
({redeemableSummary.totalCount}, ${formatUSDC(redeemableSummary.totalValue)})
</Button>
)}
</div>
@@ -1238,13 +1238,13 @@ const PositionList: React.FC = () => {
<span>
{' '}
<span style={{ fontWeight: 600 }}>
{formatUSDC(positionTotals.totalInitialValue.toString())} USDC
${formatUSDC(positionTotals.totalInitialValue.toString())}
</span>
</span>
<span>
{' '}
<span style={{ fontWeight: 600 }}>
{formatUSDC(positionTotals.totalCurrentValue.toString())} USDC
${formatUSDC(positionTotals.totalCurrentValue.toString())}
</span>
</span>
<span>
@@ -1256,7 +1256,7 @@ const PositionList: React.FC = () => {
}}
>
{positionTotals.totalPnl >= 0 ? '+' : ''}
{formatUSDC(positionTotals.totalPnl.toString())} USDC
${formatUSDC(positionTotals.totalPnl.toString())}
</span>
</span>
<span>
@@ -1268,7 +1268,7 @@ const PositionList: React.FC = () => {
}}
>
{positionTotals.totalRealizedPnl >= 0 ? '+' : ''}
{formatUSDC(positionTotals.totalRealizedPnl.toString())} USDC
${formatUSDC(positionTotals.totalRealizedPnl.toString())}
</span>
</span>
</div>
@@ -1532,7 +1532,7 @@ const PositionList: React.FC = () => {
color: currentPnl.pnl >= 0 ? '#52c41a' : '#f5222d',
marginBottom: '4px'
}}>
{currentPnl.pnl >= 0 ? '+' : ''}{formatUSDC(currentPnl.pnl)} USDC
{currentPnl.pnl >= 0 ? '+' : ''}${formatUSDC(currentPnl.pnl)}
</div>
<div style={{
fontSize: '14px',
@@ -1572,7 +1572,7 @@ const PositionList: React.FC = () => {
</Descriptions.Item>
<Descriptions.Item label="可赎回总价值">
<span style={{ fontSize: '18px', fontWeight: 'bold', color: '#52c41a' }}>
{formatUSDC(redeemableSummary.totalValue)} USDC
${formatUSDC(redeemableSummary.totalValue)}
</span>
</Descriptions.Item>
<Descriptions.Item label="涉及账户">
@@ -1625,7 +1625,7 @@ const PositionList: React.FC = () => {
width: 120
},
{
title: '价值 (USDC)',
title: '价值 ($)',
dataIndex: 'value',
key: 'value',
align: 'right' as const,
+4 -8
View File
@@ -101,9 +101,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.totalPnl') || '总盈亏'}
value={formatUSDC(stats?.totalPnl || '0')}
prefix={stats?.totalPnl && parseFloat(stats.totalPnl) >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
prefix={<>{stats?.totalPnl && parseFloat(stats.totalPnl) >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />} $</>}
valueStyle={{ color: stats?.totalPnl && parseFloat(stats.totalPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
suffix="USDC"
loading={loading}
/>
</Card>
@@ -124,9 +123,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.avgPnl') || '平均盈亏'}
value={formatUSDC(stats?.avgPnl || '0')}
prefix={stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />}
prefix={<>{stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? <ArrowUpOutlined /> : <ArrowDownOutlined />} $</>}
valueStyle={{ color: stats?.avgPnl && parseFloat(stats.avgPnl || '0') >= 0 ? '#3f8600' : '#cf1322' }}
suffix="USDC"
loading={loading}
/>
</Card>
@@ -136,9 +134,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.maxProfit') || '最大盈利'}
value={formatUSDC(stats?.maxProfit || '0')}
prefix={<ArrowUpOutlined />}
prefix={<><ArrowUpOutlined /> $</>}
valueStyle={{ color: '#3f8600' }}
suffix="USDC"
loading={loading}
/>
</Card>
@@ -148,9 +145,8 @@ const Statistics: React.FC = () => {
<Statistic
title={t('statistics.maxLoss') || '最大亏损'}
value={formatUSDC(stats?.maxLoss || '0')}
prefix={<ArrowDownOutlined />}
prefix={<><ArrowDownOutlined /> $</>}
valueStyle={{ color: '#cf1322' }}
suffix="USDC"
loading={loading}
/>
</Card>
+6 -6
View File
@@ -154,7 +154,7 @@ const TemplateAdd: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label={t('templateAdd.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('templateAdd.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
rules={[
{ required: true, message: t('templateAdd.fixedAmountRequired') || '请输入固定跟单金额' },
@@ -193,9 +193,9 @@ const TemplateAdd: React.FC = () => {
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('templateAdd.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('templateAdd.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('templateAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。'}
tooltip={t('templateAdd.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 $1000。'}
>
<InputNumber
min={0.0001}
@@ -213,9 +213,9 @@ const TemplateAdd: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateAdd.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('templateAdd.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('templateAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
tooltip={t('templateAdd.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= $1。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
rules={[
{
validator: (_, value) => {
@@ -282,7 +282,7 @@ const TemplateAdd: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateAdd.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('templateAdd.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('templateAdd.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
+7 -7
View File
@@ -189,9 +189,9 @@ const TemplateEdit: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label={t('templateEdit.fixedAmount') || '固定跟单金额 (USDC)'}
label={t('templateEdit.fixedAmount') || '固定跟单金额 ($)'}
name="fixedAmount"
tooltip={t('templateEdit.fixedAmountTooltip') || '固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= 1 USDC。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 10 USDC。'}
tooltip={t('templateEdit.fixedAmountTooltip') || '固定金额模式下,每次跟单的固定金额,不随 Leader 订单大小变化。必须 >= $1。例如:设置为 10,则无论 Leader 买入多少,跟单金额始终为 $10。'}
rules={[
{ required: true, message: t('templateEdit.fixedAmountRequired') || '请输入固定跟单金额' },
{
@@ -229,9 +229,9 @@ const TemplateEdit: React.FC = () => {
{copyMode === 'RATIO' && (
<>
<Form.Item
label={t('templateEdit.maxOrderSize') || '单笔订单最大金额 (USDC)'}
label={t('templateEdit.maxOrderSize') || '单笔订单最大金额 ($)'}
name="maxOrderSize"
tooltip={t('templateEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。'}
tooltip={t('templateEdit.maxOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 $1000。'}
>
<InputNumber
min={0.0001}
@@ -249,9 +249,9 @@ const TemplateEdit: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateEdit.minOrderSize') || '单笔订单最小金额 (USDC)'}
label={t('templateEdit.minOrderSize') || '单笔订单最小金额 ($)'}
name="minOrderSize"
tooltip={t('templateEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
tooltip={t('templateEdit.minOrderSizeTooltip') || '比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= $1。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。'}
rules={[
{
validator: (_, value) => {
@@ -318,7 +318,7 @@ const TemplateEdit: React.FC = () => {
</Form.Item>
<Form.Item
label={t('templateEdit.minOrderDepth') || '最小订单深度 (USDC)'}
label={t('templateEdit.minOrderDepth') || '最小订单深度 ($)'}
name="minOrderDepth"
tooltip={t('templateEdit.minOrderDepthTooltip') || '检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤'}
>
+10 -10
View File
@@ -176,7 +176,7 @@ const TemplateList: React.FC = () => {
if (record.copyMode === 'RATIO') {
return `${t('templateList.ratio') || '比例'} ${record.copyRatio}x`
} else if (record.copyMode === 'FIXED' && record.fixedAmount) {
return `${t('templateList.fixedAmount') || '固定'} ${formatUSDC(record.fixedAmount)} USDC`
return `$${formatUSDC(record.fixedAmount)}`
}
return '-'
}
@@ -350,7 +350,7 @@ const TemplateList: React.FC = () => {
<div style={{ fontSize: '12px', opacity: '0.9' }}>
{template.copyMode === 'RATIO'
? `${t('templateList.ratioMode') || '比例模式'} ${(parseFloat(template.copyRatio || '0') * 100).toFixed(0).replace(/\.0+$/, '')}%`
: `${t('templateList.fixedAmountMode') || '固定金额'} ${formatUSDC(template.fixedAmount || '0')} USDC`
: `$${formatUSDC(template.fixedAmount || '0')}`
}
</div>
</div>
@@ -396,11 +396,11 @@ const TemplateList: React.FC = () => {
}}>
<span style={{ color: '#d48806' }}>{t('templateList.amountLimit') || '金额限制'}: </span>
{template.maxOrderSize && (
<span>{t('templateList.max') || '最大'} {formatUSDC(template.maxOrderSize)} USDC</span>
<span>{t('templateList.max') || '最大'} ${formatUSDC(template.maxOrderSize)}</span>
)}
{template.maxOrderSize && template.minOrderSize && <span> | </span>}
{template.minOrderSize && (
<span>{t('templateList.min') || '最小'} {formatUSDC(template.minOrderSize)} USDC</span>
<span>{t('templateList.min') || '最小'} ${formatUSDC(template.minOrderSize)}</span>
)}
{!template.maxOrderSize && !template.minOrderSize && <span style={{ color: '#bfbfbf' }}>{t('templateList.notSet') || '未设置'}</span>}
</div>
@@ -551,7 +551,7 @@ const TemplateList: React.FC = () => {
{copyMode === 'FIXED' && (
<Form.Item
label="固定跟单金额 (USDC)"
label="固定跟单金额 ($)"
name="fixedAmount"
rules={[
{ required: true, message: '请输入固定跟单金额' },
@@ -589,9 +589,9 @@ const TemplateList: React.FC = () => {
{copyMode === 'RATIO' && (
<>
<Form.Item
label="单笔订单最大金额 (USDC)"
label="单笔订单最大金额 ($)"
name="maxOrderSize"
tooltip="比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 1000 USDC。"
tooltip="比例模式下,限制单笔跟单订单的最大金额上限,用于防止跟单金额过大,控制风险。例如:设置为 1000,即使计算出的跟单金额超过 1000,也会限制为 $1000。"
>
<InputNumber
min={0.01}
@@ -609,9 +609,9 @@ const TemplateList: React.FC = () => {
</Form.Item>
<Form.Item
label="单笔订单最小金额 (USDC)"
label="单笔订单最小金额 ($)"
name="minOrderSize"
tooltip="比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= 1 USDC。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。"
tooltip="比例模式下,限制单笔跟单订单的最小金额下限,用于过滤掉金额过小的订单,避免频繁小额交易。如果填写,必须 >= $1。例如:设置为 10,如果计算出的跟单金额小于 10,则跳过该订单。"
rules={[
{
validator: (_, value) => {
@@ -698,7 +698,7 @@ const TemplateList: React.FC = () => {
<Divider></Divider>
<Form.Item
label="最小订单深度 (USDC)"
label="最小订单深度 ($)"
name="minOrderDepth"
tooltip="检查订单簿的总订单金额(买盘+卖盘),确保市场有足够的流动性。不填写则不启用此过滤"
>
+14 -2
View File
@@ -284,9 +284,21 @@ export const apiService = {
/**
*
*/
redeemPositions: (data: any) =>
redeemPositions: (data: any) =>
apiClient.post<ApiResponse<any>>('/accounts/positions/redeem', data),
/**
* USDC.e wrap pUSDV2
*/
wrapToPusd: (accountId: number) =>
apiClient.post<ApiResponse<{ transactionHash: string | null }>>('/accounts/wrap-to-pusd', { accountId }),
/**
* USDC.e V2
*/
getUsdceBalance: (accountId: number) =>
apiClient.post<ApiResponse<{ balance: string }>>('/accounts/usdce-balance', { accountId }),
},
/**