Size demo trading by a configured capital instead of full account equity
Add trading_account.capital_usdt. Position sizing will use min(capital, account equity); starting requires the capital to be set. The trading page shows and edits it.
This commit is contained in:
1 parent
005943a54c
commit
7536c9c5de
5 files changed
+97
-6
No files matched your search
@@ -0,0 +1,29 @@
|
||||
"""trading capital
|
||||
|
||||
Revision ID: c3f19d7a2e58
|
||||
Revises: a54081406f23
|
||||
Create Date: 2026-10-15 10:10:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'c3f19d7a2e58'
|
||||
down_revision: Union[str, Sequence[str], None] = 'a54081406f23'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# 用户要求只用设定的资金操盘,不动用账户全部余额(2026-10-15)
|
||||
op.add_column('trading_account', sa.Column('capital_usdt', sa.Numeric(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_column('trading_account', 'capital_usdt')
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import func, or_, select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
@@ -41,6 +42,9 @@ class StatusOut(BaseModel):
|
||||
configured: bool
|
||||
running: bool
|
||||
running_changed_at: datetime | None
|
||||
capital_usdt: Decimal | None
|
||||
# 实际用于仓位计算的资金:min(交易资金, 账户权益);没有快照时无法计算
|
||||
effective_capital: Decimal | None
|
||||
provider_id: int | None
|
||||
reasoning_effort: str | None
|
||||
# trader 进程的心跳快照;进程离线时为空
|
||||
@@ -49,6 +53,7 @@ class StatusOut(BaseModel):
|
||||
|
||||
|
||||
class ConfigIn(BaseModel):
|
||||
capital_usdt: Annotated[Decimal, Field(gt=0, decimal_places=2)] | None = None
|
||||
provider_id: int | None = None
|
||||
reasoning_effort: llm.Effort | None = None
|
||||
|
||||
@@ -108,10 +113,13 @@ async def get_status(session: Annotated[AsyncSession, Depends(get_session)]) ->
|
||||
func.coalesce(func.sum(TradingAnalysis.usage["output_tokens"].as_integer()), 0),
|
||||
).where(TradingAnalysis.started_at >= today)
|
||||
)).one()
|
||||
snapshot = await read_snapshot()
|
||||
equity = snapshot and snapshot["account"] and Decimal(snapshot["account"]["margin_balance"])
|
||||
return StatusOut(
|
||||
configured=account.api_key_encrypted is not None, running=account.running,
|
||||
running_changed_at=account.running_changed_at, provider_id=account.provider_id,
|
||||
reasoning_effort=account.reasoning_effort, snapshot=await read_snapshot(),
|
||||
running_changed_at=account.running_changed_at, capital_usdt=account.capital_usdt,
|
||||
effective_capital=min(account.capital_usdt, equity) if account.capital_usdt and equity else None,
|
||||
provider_id=account.provider_id, reasoning_effort=account.reasoning_effort, snapshot=snapshot,
|
||||
today=TodayUsage(analyses=count, limit=DAILY_ANALYSIS_LIMIT, input_tokens=input_tokens, output_tokens=output_tokens),
|
||||
)
|
||||
|
||||
@@ -120,6 +128,8 @@ async def set_running(session: AsyncSession, running: bool) -> StatusOut:
|
||||
account = await get_account(session)
|
||||
if running and account.api_key_encrypted is None:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "还没有填写模拟盘 key,请先到环境变量页填写")
|
||||
if running and account.capital_usdt is None:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, "还没有设置交易资金")
|
||||
if account.running != running:
|
||||
account.running = running
|
||||
account.running_changed_at = datetime.now(LOCAL_TZ)
|
||||
@@ -148,6 +158,19 @@ async def patch_config(body: ConfigIn, session: Annotated[AsyncSession, Depends(
|
||||
fields = body.model_dump(exclude_unset=True)
|
||||
if fields.get("provider_id") is not None and await session.get(AiProvider, fields["provider_id"]) is None:
|
||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "模型配置不存在")
|
||||
if "capital_usdt" in fields:
|
||||
capital = fields["capital_usdt"]
|
||||
if capital is None and account.running:
|
||||
raise HTTPException(status.HTTP_409_CONFLICT, "交易中不能清空交易资金")
|
||||
snapshot = await read_snapshot()
|
||||
equity = snapshot and snapshot["account"] and Decimal(snapshot["account"]["margin_balance"])
|
||||
if capital is not None and equity and capital > equity:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, f"交易资金不能超过账户权益 {equity:.2f} USDT")
|
||||
if capital != account.capital_usdt:
|
||||
session.add(TradingEvent(
|
||||
kind="capital_changed",
|
||||
message=f"交易资金设为 {capital} USDT" if capital is not None else "清空交易资金",
|
||||
))
|
||||
for key, value in fields.items():
|
||||
setattr(account, key, value)
|
||||
session.add(account)
|
||||
|
||||
@@ -20,6 +20,8 @@ class TradingAccount(Base):
|
||||
api_key_hint: Mapped[str | None] = mapped_column(String(8))
|
||||
running: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
running_changed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
# 交易资金:仓位计算按 min(交易资金, 账户权益),不动用账户的全部余额;未设置时不能开始交易
|
||||
capital_usdt: Mapped[Decimal | None] = mapped_column(Numeric)
|
||||
# 交易分析用的模型配置;为空时用默认配置
|
||||
provider_id: Mapped[int | None] = mapped_column(ForeignKey("ai_providers.id", ondelete="SET NULL"))
|
||||
reasoning_effort: Mapped[str | None] = mapped_column(String(8))
|
||||
|
||||
@@ -60,6 +60,10 @@ export interface TradingStatus {
|
||||
configured: boolean
|
||||
running: boolean
|
||||
running_changed_at: string | null
|
||||
/** 设定的交易资金,十进制字符串;未设置时不能开始交易 */
|
||||
capital_usdt: string | null
|
||||
/** 实际用于仓位计算的资金:min(交易资金, 账户权益) */
|
||||
effective_capital: string | null
|
||||
provider_id: number | null
|
||||
reasoning_effort: Effort | null
|
||||
/** trader 进程离线时为空 */
|
||||
@@ -109,7 +113,7 @@ export function stopTrading(): Promise<TradingStatus> {
|
||||
return http.post("/trading/stop")
|
||||
}
|
||||
|
||||
export function patchConfig(body: { provider_id?: number | null, reasoning_effort?: Effort | null }): Promise<TradingStatus> {
|
||||
export function patchConfig(body: { capital_usdt?: string | null, provider_id?: number | null, reasoning_effort?: Effort | null }): Promise<TradingStatus> {
|
||||
return http.patch("/trading/config", body)
|
||||
}
|
||||
|
||||
|
||||
@@ -139,6 +139,22 @@ async function toggleRunning() {
|
||||
}
|
||||
}
|
||||
|
||||
async function editCapital() {
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
`仓位按交易资金计算:单笔风险短线 1%、长线 2%,所有仓位名义价值合计不超过 3 倍(逐仓 3 倍时保证金合计不超过交易资金)。不能超过账户权益 ${money(account.value?.margin_balance)} USDT;账户权益低于它时按权益计算。`,
|
||||
"设置交易资金(USDT)",
|
||||
{
|
||||
inputValue: status.value!.capital_usdt ? num(status.value!.capital_usdt) : "",
|
||||
inputPattern: /^(?:[1-9]\d*|0)(?:\.\d{1,2})?$/,
|
||||
inputErrorMessage: "请输入正数,最多两位小数",
|
||||
confirmButtonText: "保存"
|
||||
}
|
||||
)
|
||||
status.value = await patchConfig({ capital_usdt: value })
|
||||
ElMessage.success("已保存")
|
||||
await refreshEvents()
|
||||
}
|
||||
|
||||
async function changeProvider(providerId: number | null) {
|
||||
status.value = await patchConfig({ provider_id: providerId })
|
||||
}
|
||||
@@ -191,14 +207,28 @@ onBeforeUnmount(() => clearInterval(timer))
|
||||
{{ status.running ? "交易中" : "已停止" }}
|
||||
</el-tag>
|
||||
<el-button
|
||||
:type="status.running ? 'danger' : 'primary'" :loading="toggling" :disabled="!status.configured"
|
||||
:type="status.running ? 'danger' : 'primary'" :loading="toggling"
|
||||
:disabled="!status.running && (!status.configured || !status.capital_usdt)"
|
||||
@click="toggleRunning"
|
||||
>
|
||||
{{ status.running ? "停止" : "开始行情交易" }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>权益</span>{{ money(account?.margin_balance) }} USDT
|
||||
<span>交易资金</span>
|
||||
<template v-if="status.capital_usdt">
|
||||
{{ money(status.capital_usdt) }} USDT
|
||||
<span v-if="status.effective_capital && Number(status.effective_capital) < Number(status.capital_usdt)" class="text-[var(--el-color-warning)]!">
|
||||
(权益不足,按 {{ money(status.effective_capital) }})
|
||||
</span>
|
||||
</template>
|
||||
<b v-else class="text-[var(--el-color-danger)]">未设置</b>
|
||||
<el-link type="primary" underline="never" class="ml-1" @click="editCapital">
|
||||
设置
|
||||
</el-link>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>账户权益</span>{{ money(account?.margin_balance) }} USDT
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span>可用</span>{{ money(account?.available_balance) }} USDT
|
||||
@@ -229,6 +259,9 @@ onBeforeUnmount(() => clearInterval(timer))
|
||||
<el-alert v-if="!status.configured" type="warning" :closable="false" show-icon>
|
||||
还没有填写币安模拟盘 key,请先到「环境变量」页填写。
|
||||
</el-alert>
|
||||
<el-alert v-else-if="!status.capital_usdt" type="warning" :closable="false" show-icon>
|
||||
还没有设置交易资金。引擎只按交易资金计算仓位,不会动用账户的全部余额;设置后才能开始行情交易。
|
||||
</el-alert>
|
||||
<el-alert v-else-if="snapshot?.error" type="error" :closable="false" show-icon>
|
||||
同步模拟盘失败:{{ snapshot.error }}{{ snapshot.synced_at ? `(以下为 ${formatTime(snapshot.synced_at, "HH:mm:ss")} 的数据)` : "" }}
|
||||
</el-alert>
|
||||
|
||||
Reference in new issue
Block a user