feat(stats): 添加交易统计API和前端展示
在后端新增了交易统计API,支持获取最近N天的交易数据,包括按交易对和按小时的聚合统计。同时,前端组件StatsDashboard更新,展示交易统计信息,包括净盈亏、胜率及策略建议。此改动旨在提升交易数据分析能力,帮助用户更好地理解交易表现。
This commit is contained in:
parent
e2e7effca2
commit
dfe29d70dc
8
. cursorignore
Normal file
8
. cursorignore
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
node_modules
|
||||||
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
.git
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
@ -11,7 +11,7 @@ project_root = Path(__file__).parent.parent.parent.parent
|
||||||
sys.path.insert(0, str(project_root))
|
sys.path.insert(0, str(project_root))
|
||||||
sys.path.insert(0, str(project_root / 'backend'))
|
sys.path.insert(0, str(project_root / 'backend'))
|
||||||
|
|
||||||
from database.models import AccountSnapshot, Trade, MarketScan, TradingSignal, Account
|
from database.models import AccountSnapshot, Trade, MarketScan, TradingSignal, Account, TradeStats
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from api.auth_deps import get_account_id, get_admin_user
|
from api.auth_deps import get_account_id, get_admin_user
|
||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
@ -72,6 +72,117 @@ async def get_admin_dashboard_stats(user: Dict[str, Any] = Depends(get_admin_use
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_daily_by_symbol(daily: list) -> list:
|
||||||
|
"""将 daily(按 date+symbol)聚合成按 symbol 的汇总。"""
|
||||||
|
from collections import defaultdict
|
||||||
|
agg = defaultdict(lambda: {"trade_count": 0, "win_count": 0, "loss_count": 0, "net_pnl": 0.0})
|
||||||
|
for row in daily:
|
||||||
|
sym = (row.get("symbol") or "").strip()
|
||||||
|
if not sym:
|
||||||
|
continue
|
||||||
|
agg[sym]["trade_count"] += int(row.get("trade_count") or 0)
|
||||||
|
agg[sym]["win_count"] += int(row.get("win_count") or 0)
|
||||||
|
agg[sym]["loss_count"] += int(row.get("loss_count") or 0)
|
||||||
|
try:
|
||||||
|
agg[sym]["net_pnl"] += float(row.get("net_pnl") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
out = []
|
||||||
|
for symbol, v in agg.items():
|
||||||
|
tc = v["trade_count"]
|
||||||
|
win_rate = (100.0 * v["win_count"] / tc) if tc > 0 else 0.0
|
||||||
|
out.append({
|
||||||
|
"symbol": symbol,
|
||||||
|
"trade_count": tc,
|
||||||
|
"win_count": v["win_count"],
|
||||||
|
"loss_count": v["loss_count"],
|
||||||
|
"net_pnl": round(v["net_pnl"], 4),
|
||||||
|
"win_rate_pct": round(win_rate, 1),
|
||||||
|
})
|
||||||
|
return sorted(out, key=lambda x: (-x["net_pnl"], -x["trade_count"]))
|
||||||
|
|
||||||
|
|
||||||
|
def _aggregate_hourly(by_hour: list) -> list:
|
||||||
|
"""将 by_hour(按 date+hour)聚合成按 hour 0-23 的汇总。"""
|
||||||
|
from collections import defaultdict
|
||||||
|
agg = defaultdict(lambda: {"trade_count": 0, "net_pnl": 0.0})
|
||||||
|
for row in by_hour:
|
||||||
|
h = row.get("hour")
|
||||||
|
if h is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
h = int(h)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if 0 <= h <= 23:
|
||||||
|
agg[h]["trade_count"] += int(row.get("trade_count") or 0)
|
||||||
|
try:
|
||||||
|
agg[h]["net_pnl"] += float(row.get("net_pnl") or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
return [{"hour": h, "trade_count": agg[h]["trade_count"], "net_pnl": round(agg[h]["net_pnl"], 4)} for h in range(24)]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_suggestions(by_symbol: list) -> dict:
|
||||||
|
"""
|
||||||
|
根据按交易对汇总生成白名单/黑名单建议(仅展示,不自动改策略)。
|
||||||
|
- 黑名单:净亏且笔数多 → 建议降权或观察
|
||||||
|
- 白名单:净盈且胜率较高、笔数足够 → 可优先考虑
|
||||||
|
"""
|
||||||
|
blacklist = []
|
||||||
|
whitelist = []
|
||||||
|
for row in by_symbol:
|
||||||
|
sym = row.get("symbol", "")
|
||||||
|
tc = int(row.get("trade_count") or 0)
|
||||||
|
net_pnl = float(row.get("net_pnl") or 0)
|
||||||
|
win_rate = float(row.get("win_rate_pct") or 0)
|
||||||
|
if tc < 2:
|
||||||
|
continue
|
||||||
|
if net_pnl < 0:
|
||||||
|
blacklist.append({
|
||||||
|
"symbol": sym,
|
||||||
|
"trade_count": tc,
|
||||||
|
"net_pnl": round(net_pnl, 2),
|
||||||
|
"win_rate_pct": round(win_rate, 1),
|
||||||
|
"suggestion": "近期净亏且笔数较多,建议降权或观察后再开仓",
|
||||||
|
})
|
||||||
|
elif net_pnl > 0 and win_rate >= 50:
|
||||||
|
whitelist.append({
|
||||||
|
"symbol": sym,
|
||||||
|
"trade_count": tc,
|
||||||
|
"net_pnl": round(net_pnl, 2),
|
||||||
|
"win_rate_pct": round(win_rate, 1),
|
||||||
|
"suggestion": "近期净盈且胜率尚可,可优先考虑",
|
||||||
|
})
|
||||||
|
return {"blacklist": blacklist, "whitelist": whitelist}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/trade-stats")
|
||||||
|
async def get_trade_stats(
|
||||||
|
days: int = Query(7, ge=1, le=90),
|
||||||
|
account_id: int = Depends(get_account_id),
|
||||||
|
):
|
||||||
|
"""获取交易统计:最近 N 天按交易对、按小时聚合(来自 trade_stats_daily / trade_stats_time_bucket)。
|
||||||
|
返回原始 daily/by_hour、按交易对汇总 by_symbol、按小时汇总 hourly_agg、以及白名单/黑名单建议。"""
|
||||||
|
try:
|
||||||
|
daily = TradeStats.get_daily_stats(account_id=account_id, days=days)
|
||||||
|
by_hour = TradeStats.get_hourly_stats(account_id=account_id, days=days)
|
||||||
|
by_symbol = _aggregate_daily_by_symbol(daily)
|
||||||
|
hourly_agg = _aggregate_hourly(by_hour)
|
||||||
|
suggestions = _build_suggestions(by_symbol)
|
||||||
|
return {
|
||||||
|
"days": days,
|
||||||
|
"daily": daily,
|
||||||
|
"by_hour": by_hour,
|
||||||
|
"by_symbol": by_symbol,
|
||||||
|
"hourly_agg": hourly_agg,
|
||||||
|
"suggestions": suggestions,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("get_trade_stats 失败")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/performance")
|
@router.get("/performance")
|
||||||
async def get_performance_stats(
|
async def get_performance_stats(
|
||||||
days: int = Query(7, ge=1, le=365),
|
days: int = Query(7, ge=1, le=365),
|
||||||
|
|
|
||||||
|
|
@ -1485,6 +1485,48 @@ class TradeStats:
|
||||||
if daily or hourly:
|
if daily or hourly:
|
||||||
TradeStats._upsert_stats(aid, daily or {}, hourly or {})
|
TradeStats._upsert_stats(aid, daily or {}, hourly or {})
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_daily_stats(account_id: int = None, days: int = 7):
|
||||||
|
"""查询最近 N 天按交易对聚合统计,供 API/仪表盘展示。表不存在或无数据时返回 []。"""
|
||||||
|
aid = int(account_id or DEFAULT_ACCOUNT_ID)
|
||||||
|
try:
|
||||||
|
rows = db.execute_query(
|
||||||
|
"""SELECT trade_date, symbol, trade_count, win_count, loss_count,
|
||||||
|
gross_pnl, net_pnl, total_commission, avg_pnl_per_trade
|
||||||
|
FROM trade_stats_daily
|
||||||
|
WHERE account_id = %s AND trade_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||||
|
ORDER BY trade_date DESC, net_pnl DESC
|
||||||
|
LIMIT 500""",
|
||||||
|
(aid, max(1, int(days))),
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[TradeStats] get_daily_stats 失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_hourly_stats(account_id: int = None, days: int = 7):
|
||||||
|
"""查询最近 N 天按小时聚合统计,供 API/仪表盘展示。表不存在或无数据时返回 []。"""
|
||||||
|
aid = int(account_id or DEFAULT_ACCOUNT_ID)
|
||||||
|
try:
|
||||||
|
rows = db.execute_query(
|
||||||
|
"""SELECT trade_date, hour, trade_count, win_count, loss_count,
|
||||||
|
gross_pnl, net_pnl, total_commission
|
||||||
|
FROM trade_stats_time_bucket
|
||||||
|
WHERE account_id = %s AND trade_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)
|
||||||
|
ORDER BY trade_date DESC, hour ASC
|
||||||
|
LIMIT 500""",
|
||||||
|
(aid, max(1, int(days))),
|
||||||
|
)
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
return [dict(r) for r in rows]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"[TradeStats] get_hourly_stats 失败: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
class AccountSnapshot:
|
class AccountSnapshot:
|
||||||
"""账户快照模型"""
|
"""账户快照模型"""
|
||||||
|
|
|
||||||
|
|
@ -687,3 +687,112 @@
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 交易统计卡片 */
|
||||||
|
.stats-card {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card h3 {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card h4 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #555;
|
||||||
|
margin: 1rem 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card h4:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table-wrap {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table th,
|
||||||
|
.stats-table td {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
background: #f1f3f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table td.positive {
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-table td.negative {
|
||||||
|
color: #dc3545;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-symbol {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-empty {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hourly-table .stats-table {
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-section h4 {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-block {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-block.blacklist {
|
||||||
|
background: #fff5f5;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-block.whitelist {
|
||||||
|
background: #f0fff4;
|
||||||
|
border-left: 4px solid #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-block ul {
|
||||||
|
margin: 0.5rem 0 0 1rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-block li {
|
||||||
|
margin: 0.35rem 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestion-symbol {
|
||||||
|
font-weight: 700;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import './StatsDashboard.css'
|
||||||
const StatsDashboard = () => {
|
const StatsDashboard = () => {
|
||||||
const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID
|
const accountId = useSelector(selectAccountId) // 从 Redux 获取当前账号ID
|
||||||
const [dashboardData, setDashboardData] = useState(null)
|
const [dashboardData, setDashboardData] = useState(null)
|
||||||
|
const [tradeStats, setTradeStats] = useState(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [closingSymbol, setClosingSymbol] = useState(null)
|
const [closingSymbol, setClosingSymbol] = useState(null)
|
||||||
const [sltpSymbol, setSltpSymbol] = useState(null)
|
const [sltpSymbol, setSltpSymbol] = useState(null)
|
||||||
|
|
@ -55,10 +56,12 @@ const StatsDashboard = () => {
|
||||||
|
|
||||||
loadDashboard()
|
loadDashboard()
|
||||||
loadTradingConfig()
|
loadTradingConfig()
|
||||||
|
loadTradeStats()
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
loadDashboard()
|
loadDashboard()
|
||||||
loadTradingConfig() // 同时刷新配置
|
loadTradingConfig() // 同时刷新配置
|
||||||
|
loadTradeStats()
|
||||||
}
|
}
|
||||||
}, 30000) // 每30秒刷新
|
}, 30000) // 每30秒刷新
|
||||||
|
|
||||||
|
|
@ -91,6 +94,17 @@ const StatsDashboard = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadTradeStats = async () => {
|
||||||
|
if (!accountId) return
|
||||||
|
try {
|
||||||
|
const data = await api.getTradeStats(7)
|
||||||
|
setTradeStats(data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load trade stats:', error)
|
||||||
|
setTradeStats(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleClosePosition = async (symbol) => {
|
const handleClosePosition = async (symbol) => {
|
||||||
if (!window.confirm(`确定要平仓 ${symbol} 吗?`)) {
|
if (!window.confirm(`确定要平仓 ${symbol} 吗?`)) {
|
||||||
return
|
return
|
||||||
|
|
@ -834,6 +848,108 @@ const StatsDashboard = () => {
|
||||||
<div>暂无持仓</div>
|
<div>暂无持仓</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 交易统计(最近 7 天):按交易对净盈亏/胜率、按小时净盈亏、白名单/黑名单建议 */}
|
||||||
|
<div className="dashboard-card stats-card">
|
||||||
|
<h3>交易统计(最近 7 天)</h3>
|
||||||
|
{tradeStats ? (
|
||||||
|
<>
|
||||||
|
<div className="stats-section">
|
||||||
|
<h4>按交易对:净盈亏、胜率</h4>
|
||||||
|
{(tradeStats.by_symbol && tradeStats.by_symbol.length > 0) ? (
|
||||||
|
<div className="stats-table-wrap">
|
||||||
|
<table className="stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>交易对</th>
|
||||||
|
<th>笔数</th>
|
||||||
|
<th>胜率%</th>
|
||||||
|
<th>净盈亏(USDT)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tradeStats.by_symbol.map((row) => (
|
||||||
|
<tr key={row.symbol}>
|
||||||
|
<td className="stats-symbol">{row.symbol}</td>
|
||||||
|
<td>{row.trade_count}</td>
|
||||||
|
<td>{row.win_rate_pct}</td>
|
||||||
|
<td className={Number(row.net_pnl) >= 0 ? 'positive' : 'negative'}>
|
||||||
|
{Number(row.net_pnl) >= 0 ? '+' : ''}{Number(row.net_pnl).toFixed(2)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="stats-empty">暂无按交易对统计(需先运行统计聚合脚本或策略内聚合)</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="stats-section">
|
||||||
|
<h4>按小时:净盈亏(0–23 时,北京时间)</h4>
|
||||||
|
{(tradeStats.hourly_agg && tradeStats.hourly_agg.length > 0) ? (
|
||||||
|
<div className="stats-table-wrap hourly-table">
|
||||||
|
<table className="stats-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>小时</th>
|
||||||
|
<th>笔数</th>
|
||||||
|
<th>净盈亏(USDT)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{tradeStats.hourly_agg.map((row) => (
|
||||||
|
<tr key={row.hour}>
|
||||||
|
<td>{row.hour}:00</td>
|
||||||
|
<td>{row.trade_count}</td>
|
||||||
|
<td className={Number(row.net_pnl) >= 0 ? 'positive' : 'negative'}>
|
||||||
|
{Number(row.net_pnl) >= 0 ? '+' : ''}{Number(row.net_pnl).toFixed(2)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="stats-empty">暂无按小时统计</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{tradeStats.suggestions && (tradeStats.suggestions.blacklist?.length > 0 || tradeStats.suggestions.whitelist?.length > 0) && (
|
||||||
|
<div className="stats-section suggestions-section">
|
||||||
|
<h4>策略建议(仅供参考,不自动改策略)</h4>
|
||||||
|
{tradeStats.suggestions.blacklist?.length > 0 && (
|
||||||
|
<div className="suggestion-block blacklist">
|
||||||
|
<strong>建议降权/观察:</strong> 近期净亏且笔数较多的标的,可考虑降低仓位或暂时跳过。
|
||||||
|
<ul>
|
||||||
|
{tradeStats.suggestions.blacklist.map((item) => (
|
||||||
|
<li key={item.symbol}>
|
||||||
|
<span className="suggestion-symbol">{item.symbol}</span>
|
||||||
|
笔数 {item.trade_count},净盈亏 {item.net_pnl} USDT,胜率 {item.win_rate_pct}% — {item.suggestion}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tradeStats.suggestions.whitelist?.length > 0 && (
|
||||||
|
<div className="suggestion-block whitelist">
|
||||||
|
<strong>可优先考虑:</strong> 近期净盈且胜率尚可的标的。
|
||||||
|
<ul>
|
||||||
|
{tradeStats.suggestions.whitelist.map((item) => (
|
||||||
|
<li key={item.symbol}>
|
||||||
|
<span className="suggestion-symbol">{item.symbol}</span>
|
||||||
|
笔数 {item.trade_count},净盈亏 +{item.net_pnl} USDT,胜率 {item.win_rate_pct}%
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p className="stats-empty">加载统计中…(若长期无数据,请先运行 scripts/aggregate_trade_stats.py 或依赖定时任务更新)</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,15 @@ export const api = {
|
||||||
return response.json();
|
return response.json();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTradeStats: async (days = 7) => {
|
||||||
|
const response = await fetch(buildUrl(`/api/stats/trade-stats?days=${days}`), { headers: withAccountHeaders() });
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json().catch(() => ({ detail: '获取交易统计失败' }));
|
||||||
|
throw new Error(error.detail || '获取交易统计失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
},
|
||||||
|
|
||||||
getAdminDashboard: async () => {
|
getAdminDashboard: async () => {
|
||||||
const response = await fetch(buildUrl('/api/stats/admin/dashboard'), { headers: withAuthHeaders() });
|
const response = await fetch(buildUrl('/api/stats/admin/dashboard'), { headers: withAuthHeaders() });
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user