Add Binance demo trading key settings and signed demo client
Store demo API key/secret encrypted in a single-row trading_account table, expose get/put/delete/test endpoints under /api/settings/exchange, and add a settings card to enter and test the key. The client is hard-wired to demo-fapi.binance.com.
This commit is contained in:
1 parent
438d6a00d9
commit
20fd5a98b2
8 files changed
+384
-5
No files matched your search
@@ -63,3 +63,37 @@ export function testProvider(id: number): Promise<TestResult> {
|
||||
// 模型接口读超时 600s 且 SDK 会重试 2 次,前端放宽等待时间
|
||||
return http.post(`/settings/providers/${id}/test`, null, { timeout: 1900000 })
|
||||
}
|
||||
|
||||
export interface Exchange {
|
||||
configured: boolean
|
||||
api_key_hint: string | null
|
||||
running: boolean
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface ExchangeTestResult {
|
||||
ok: boolean
|
||||
elapsed_ms: number
|
||||
available_usdt: string | null
|
||||
wallet_usdt: string | null
|
||||
/** 双向持仓模式;引擎使用单向持仓,启动时会切换 */
|
||||
dual_side_position: boolean | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export function getExchange(): Promise<Exchange> {
|
||||
return http.get("/settings/exchange")
|
||||
}
|
||||
|
||||
export function putExchange(body: { api_key: string, api_secret: string }): Promise<Exchange> {
|
||||
return http.put("/settings/exchange", body)
|
||||
}
|
||||
|
||||
export function deleteExchange(): Promise<Exchange> {
|
||||
return http.delete("/settings/exchange")
|
||||
}
|
||||
|
||||
export function testExchange(): Promise<ExchangeTestResult> {
|
||||
// 后端串行请求两个接口,单次超时 10s
|
||||
return http.post("/settings/exchange/test", null, { timeout: 30000 })
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { Provider, ProviderForm } from "@/apis/settings"
|
||||
import { createProvider, deleteProvider, EFFORTS, listProviders, patchProvider, testProvider } from "@/apis/settings"
|
||||
import type { Exchange, Provider, ProviderForm } from "@/apis/settings"
|
||||
import { createProvider, deleteExchange, deleteProvider, EFFORTS, getExchange, listProviders, patchProvider, putExchange, testExchange, testProvider } from "@/apis/settings"
|
||||
import { formatTime } from "@/utils/text"
|
||||
|
||||
const PROTOCOL_LABELS = { openai: "OpenAI 格式(/v1/chat/completions)", anthropic: "Claude 格式(/v1/messages)" } as const
|
||||
@@ -76,6 +76,57 @@ async function test(p: Provider) {
|
||||
}
|
||||
|
||||
load()
|
||||
|
||||
// ---------- 币安模拟盘 ----------
|
||||
|
||||
const exchange = ref<Exchange>()
|
||||
const exchangeDialogVisible = ref(false)
|
||||
const exchangeForm = ref({ api_key: "", api_secret: "" })
|
||||
const exchangeSaving = ref(false)
|
||||
const exchangeTesting = ref(false)
|
||||
|
||||
async function loadExchange() {
|
||||
exchange.value = await getExchange()
|
||||
}
|
||||
|
||||
function openExchangeEdit() {
|
||||
exchangeForm.value = { api_key: "", api_secret: "" }
|
||||
exchangeDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveExchange() {
|
||||
exchangeSaving.value = true
|
||||
try {
|
||||
exchange.value = await putExchange(exchangeForm.value)
|
||||
exchangeDialogVisible.value = false
|
||||
ElMessage.success("已保存")
|
||||
} finally {
|
||||
exchangeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeExchange() {
|
||||
await ElMessageBox.confirm("删除模拟盘 API key?", "删除确认", { type: "warning" })
|
||||
exchange.value = await deleteExchange()
|
||||
ElMessage.success("已删除")
|
||||
}
|
||||
|
||||
async function runExchangeTest() {
|
||||
exchangeTesting.value = true
|
||||
try {
|
||||
const r = await testExchange()
|
||||
if (r.ok) {
|
||||
const mode = r.dual_side_position ? "双向持仓(开始交易时会切换为单向)" : "单向持仓"
|
||||
ElMessage.success({ message: `连接成功,用时 ${r.elapsed_ms}ms;可用 ${r.available_usdt} USDT,钱包余额 ${r.wallet_usdt} USDT;${mode}`, duration: 8000 })
|
||||
} else {
|
||||
ElMessage.error({ message: `连接失败(${r.elapsed_ms}ms):${r.error}`, duration: 8000 })
|
||||
}
|
||||
} finally {
|
||||
exchangeTesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
loadExchange()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -193,5 +244,59 @@ load()
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<span class="text-lg font-bold">币安模拟盘</span>
|
||||
<span class="ml-3 text-xs text-[var(--el-text-color-secondary)]">只连接币安模拟盘(demo-fapi.binance.com);在 demo.binance.com 的 API 管理创建 key,并开启合约交易权限</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-button :disabled="!exchange?.configured" :loading="exchangeTesting" @click="runExchangeTest">
|
||||
测试连接
|
||||
</el-button>
|
||||
<el-button type="primary" :disabled="exchange?.running" @click="openExchangeEdit">
|
||||
{{ exchange?.configured ? "修改 key" : "填写 key" }}
|
||||
</el-button>
|
||||
<el-button v-if="exchange?.configured" type="danger" plain :disabled="exchange.running" @click="removeExchange">
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-descriptions v-if="exchange" :column="3" border>
|
||||
<el-descriptions-item label="API Key">
|
||||
{{ exchange.configured ? `****${exchange.api_key_hint}` : "未配置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag v-if="exchange.running" type="success" size="small">
|
||||
交易中(停止后才能修改 key)
|
||||
</el-tag>
|
||||
<span v-else>未运行</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ exchange.updated_at ? formatTime(exchange.updated_at, "MM-DD HH:mm") : "-" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-dialog v-model="exchangeDialogVisible" title="币安模拟盘 API key" width="560px">
|
||||
<el-form :model="exchangeForm" label-width="100px">
|
||||
<el-form-item label="API Key" required>
|
||||
<el-input v-model="exchangeForm.api_key" type="password" show-password autocomplete="new-password" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Secret Key" required>
|
||||
<el-input v-model="exchangeForm.api_secret" type="password" show-password autocomplete="new-password" />
|
||||
<div class="text-xs text-[var(--el-text-color-secondary)]">
|
||||
两项都要填;加密保存,之后不再显示
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="exchangeDialogVisible = false">
|
||||
取消
|
||||
</el-button>
|
||||
<el-button type="primary" :loading="exchangeSaving" :disabled="!exchangeForm.api_key.trim() || !exchangeForm.api_secret.trim()" @click="saveExchange">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user