This commit is contained in:
aaron 2026-02-11 00:05:39 +08:00
parent 1c10361a55
commit 6dea177565
4 changed files with 158 additions and 21 deletions

View File

@ -162,6 +162,26 @@ async def get_statistics(
raise HTTPException(status_code=500, detail=str(e))
@router.get("/account")
async def get_account_status():
"""
获取账户状态
返回账户余额已用保证金可用保证金等信息
"""
try:
service = get_paper_trading_service()
account = service.get_account_status()
return {
"success": True,
"account": account
}
except Exception as e:
logger.error(f"获取账户状态失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/statistics/by-grade")
async def get_statistics_by_grade():
"""按信号等级获取统计"""

View File

@ -112,6 +112,11 @@ class Settings(BaseSettings):
# 模拟交易配置
paper_trading_enabled: bool = True # 是否启用模拟交易
paper_trading_initial_balance: float = 10000 # 初始本金 (USDT)
paper_trading_leverage: int = 10 # 杠杆倍数
paper_trading_margin_per_order: float = 1000 # 每单保证金 (USDT)
paper_trading_max_orders: int = 10 # 最大持仓+挂单总数
# 废弃的配置(保留兼容性)
paper_trading_position_a: float = 1000 # A级信号仓位 (USDT)
paper_trading_position_b: float = 500 # B级信号仓位 (USDT)
paper_trading_position_c: float = 200 # C级信号仓位 (USDT)

View File

@ -11,15 +11,6 @@ from app.config import get_settings
from app.utils.logger import logger
# 仓位大小配置
POSITION_SIZE = {
'A': 1000, # A级信号 1000 USDT
'B': 500, # B级信号 500 USDT
'C': 200, # C级信号 200 USDT
'D': 0 # D级信号不开仓
}
class PaperTradingService:
"""模拟交易服务"""
@ -28,6 +19,12 @@ class PaperTradingService:
self.settings = get_settings()
self.active_orders: Dict[str, PaperOrder] = {} # 内存缓存活跃订单
# 合约交易配置
self.initial_balance = self.settings.paper_trading_initial_balance # 初始本金
self.leverage = self.settings.paper_trading_leverage # 杠杆倍数
self.margin_per_order = self.settings.paper_trading_margin_per_order # 每单保证金
self.max_orders = self.settings.paper_trading_max_orders # 最大订单数
# 确保表已创建
self._ensure_table_exists()
@ -89,16 +86,17 @@ class PaperTradingService:
entry_price = signal.get('entry_price') or signal.get('price', 0)
# === 限制检查 ===
# 1. 同一交易对同一方向最多 3 个订单
# 1. 检查总订单数(持仓+挂单)是否超过最大限制
total_orders = len(self.active_orders)
if total_orders >= self.max_orders:
logger.info(f"订单限制: 已达到最大订单数 {self.max_orders},跳过")
return None
# 2. 检查是否有接近的挂单(价格差距 < 1%
same_direction_orders = [
order for order in self.active_orders.values()
if order.symbol == symbol and order.side == side
]
if len(same_direction_orders) >= 3:
logger.info(f"订单限制: {symbol} {side.value} 方向已有 {len(same_direction_orders)} 个订单,跳过")
return None
# 2. 检查是否有接近的挂单(价格差距 < 1%
pending_orders = [
order for order in same_direction_orders
if order.status == OrderStatus.PENDING
@ -115,10 +113,10 @@ class PaperTradingService:
logger.info(f"D级信号不开仓: {signal.get('symbol')}")
return None
# 确定仓位大小
quantity = POSITION_SIZE.get(grade, 0)
if quantity == 0:
return None
# 固定使用保证金(不再根据等级区分)
margin = self.margin_per_order # 每单固定 1000 USDT 保证金
position_value = margin * self.leverage # 持仓价值 = 保证金 × 杠杆
quantity = position_value # 订单数量(以 USDT 计价)
# 确定入场类型
entry_type_str = signal.get('entry_type', 'market')
@ -170,7 +168,8 @@ class PaperTradingService:
entry_type_text = "现价" if entry_type == EntryType.MARKET else "挂单"
status_text = "已开仓" if status == OrderStatus.OPEN else "等待触发"
logger.info(f"创建模拟订单: {order_id} | {symbol} {side.value} [{entry_type_text}] @ ${entry_price:,.2f} | {status_text} | 仓位: ${quantity}")
logger.info(f"创建模拟订单: {order_id} | {symbol} {side.value} [{entry_type_text}] @ ${entry_price:,.2f} | {status_text}")
logger.info(f" 保证金: ${margin:,.0f} | 杠杆: {self.leverage}x | 持仓价值: ${position_value:,.0f} | 当前订单数: {len(self.active_orders)}/{self.max_orders}")
return order
except Exception as e:
@ -574,6 +573,56 @@ class PaperTradingService:
'by_symbol': {}
}
def get_account_status(self) -> Dict[str, Any]:
"""
获取账户状态
Returns:
账户状态信息包括余额已用保证金可用保证金等
"""
# 计算已用保证金(每个活跃订单占用固定保证金)
active_count = len(self.active_orders)
used_margin = active_count * self.margin_per_order
# 计算已实现盈亏(从历史订单)
db = db_service.get_session()
try:
closed_orders = db.query(PaperOrder).filter(
PaperOrder.status.in_([
OrderStatus.CLOSED_TP,
OrderStatus.CLOSED_SL,
OrderStatus.CLOSED_MANUAL
])
).all()
realized_pnl = sum(o.pnl_amount for o in closed_orders)
finally:
db.close()
# 计算当前余额
current_balance = self.initial_balance + realized_pnl
# 计算可用保证金
available_margin = current_balance - used_margin
# 计算可开仓数
available_orders = self.max_orders - active_count
return {
'initial_balance': self.initial_balance,
'realized_pnl': round(realized_pnl, 2),
'current_balance': round(current_balance, 2),
'used_margin': round(used_margin, 2),
'available_margin': round(available_margin, 2),
'leverage': self.leverage,
'margin_per_order': self.margin_per_order,
'active_orders': active_count,
'max_orders': self.max_orders,
'available_orders': available_orders,
'total_position_value': round(used_margin * self.leverage, 2),
'margin_ratio': round((used_margin / current_balance * 100), 2) if current_balance > 0 else 0
}
def _calculate_grade_statistics(self, orders: List[PaperOrder]) -> Dict[str, Any]:
"""按信号等级统计"""
result = {}

View File

@ -517,6 +517,46 @@
<!-- 统计卡片 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">账户余额</div>
<div class="stat-value" :class="account.current_balance >= account.initial_balance ? 'positive' : 'negative'">
${{ account.current_balance?.toFixed(2) || '0.00' }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">已用保证金</div>
<div class="stat-value">
${{ account.used_margin?.toFixed(2) || '0.00' }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">可用保证金</div>
<div class="stat-value">
${{ account.available_margin?.toFixed(2) || '0.00' }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">杠杆 / 保证金率</div>
<div class="stat-value">
{{ account.leverage }}x / {{ account.margin_ratio?.toFixed(1) || '0' }}%
</div>
</div>
<div class="stat-card">
<div class="stat-label">持仓数 / 最大</div>
<div class="stat-value">
{{ account.active_orders || 0 }} / {{ account.max_orders || 10 }}
</div>
</div>
<div class="stat-card">
<div class="stat-label">总持仓价值</div>
<div class="stat-value">
${{ account.total_position_value?.toFixed(2) || '0.00' }}
</div>
</div>
</div>
<!-- 交易统计 -->
<div class="stats-grid" style="margin-top: 16px;">
<div class="stat-card">
<div class="stat-label">总交易数</div>
<div class="stat-value">{{ stats.total_trades }}</div>
@ -526,7 +566,7 @@
<div class="stat-value">{{ stats.win_rate.toFixed(1) }}%</div>
</div>
<div class="stat-card">
<div class="stat-label">盈亏</div>
<div class="stat-label">已实现盈亏</div>
<div class="stat-value" :class="stats.total_pnl >= 0 ? 'positive' : 'negative'">
${{ stats.total_pnl.toFixed(2) }}
</div>
@ -792,6 +832,20 @@
by_grade: {},
by_symbol: {}
},
account: {
initial_balance: 10000,
current_balance: 10000,
used_margin: 0,
available_margin: 10000,
leverage: 10,
margin_per_order: 1000,
active_orders: 0,
max_orders: 10,
available_orders: 10,
total_position_value: 0,
margin_ratio: 0,
realized_pnl: 0
},
monitorRunning: false,
latestPrices: {},
refreshInterval: null,
@ -829,6 +883,7 @@
this.fetchActiveOrders(),
this.fetchHistoryOrders(),
this.fetchStatistics(),
this.fetchAccountStatus(),
this.fetchMonitorStatus()
]);
} catch (e) {
@ -907,6 +962,14 @@
}
},
async fetchAccountStatus() {
const response = await fetch('/api/paper-trading/account');
const data = await response.json();
if (data.success) {
this.account = data.account;
}
},
async fetchMonitorStatus() {
const response = await fetch('/api/paper-trading/monitor/status');
const data = await response.json();