feat: Add System Overview tab to User Management page (admin only)

- Add backend API endpoint /api/users/system-strategies for system-wide strategy data
- Query all strategies across all users with positions, PnL, trade stats
- Add summary statistics (total strategies, running count, total capital, total PnL/ROI)
- Support filtering by status (running/stopped) and search by strategy/symbol/user
- Add System Overview tab with summary cards and detailed strategy table
- Display user, strategy name, status, symbol, capital, PnL/ROI, positions, trades, indicator, exchange, timeframe, leverage
- Add i18n translations for zh-CN and en-US
- Lazy-load strategy data when tab is first accessed
This commit is contained in:
TIANHE
2026-02-13 02:46:59 +08:00
parent f0a5af595d
commit 4c73439bd6
5 changed files with 953 additions and 103 deletions
+264
View File
@@ -4,9 +4,11 @@ User Management API Routes
Provides endpoints for user CRUD operations, role management, etc.
Only accessible by admin users.
"""
import json
from flask import Blueprint, request, jsonify, g
from app.services.user_service import get_user_service
from app.utils.auth import login_required, admin_required
from app.utils.db import get_db_connection
from app.utils.logger import get_logger
logger = get_logger(__name__)
@@ -749,3 +751,265 @@ def change_password():
except Exception as e:
logger.error(f"change_password failed: {e}")
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500
# ==================== System Overview (Admin) ====================
def _safe_json_loads(s, default=None):
"""Safely parse JSON string."""
if not s:
return default
if isinstance(s, dict):
return s
try:
return json.loads(s)
except Exception:
return default
@user_bp.route('/system-strategies', methods=['GET'])
@login_required
@admin_required
def get_system_strategies():
"""
Get all strategies across the entire system (admin only).
Returns strategy details with user info, positions, PnL, indicators, etc.
Query params:
page: int (default 1)
page_size: int (default 20, max 100)
status: str (optional, filter by status: running/stopped/all)
search: str (optional, search by strategy name/symbol/username)
"""
try:
page = request.args.get('page', 1, type=int)
page_size = request.args.get('page_size', 20, type=int)
status_filter = request.args.get('status', '', type=str).strip().lower()
search = request.args.get('search', '', type=str).strip()
page_size = min(100, max(1, page_size))
offset = (page - 1) * page_size
with get_db_connection() as db:
cur = db.cursor()
# Build WHERE clause
conditions = []
params = []
if status_filter and status_filter != 'all':
conditions.append("s.status = ?")
params.append(status_filter)
if search:
conditions.append(
"(s.strategy_name ILIKE ? OR s.symbol ILIKE ? OR u.username ILIKE ? OR u.nickname ILIKE ?)"
)
like_val = f"%{search}%"
params.extend([like_val, like_val, like_val, like_val])
where_clause = ""
if conditions:
where_clause = "WHERE " + " AND ".join(conditions)
# Get total count
count_sql = f"""
SELECT COUNT(*) as cnt
FROM qd_strategies_trading s
LEFT JOIN qd_users u ON u.id = s.user_id
{where_clause}
"""
cur.execute(count_sql, tuple(params))
total = cur.fetchone()['cnt']
# Get strategies with user info
query_sql = f"""
SELECT
s.id,
s.user_id,
s.strategy_name,
s.strategy_type,
s.market_category,
s.execution_mode,
s.status,
s.symbol,
s.timeframe,
s.initial_capital,
s.leverage,
s.market_type,
s.indicator_config,
s.trading_config,
s.exchange_config,
s.decide_interval,
s.created_at,
s.updated_at,
u.username,
u.nickname
FROM qd_strategies_trading s
LEFT JOIN qd_users u ON u.id = s.user_id
{where_clause}
ORDER BY s.status DESC, s.updated_at DESC
LIMIT ? OFFSET ?
"""
cur.execute(query_sql, tuple(params) + (page_size, offset))
strategies = cur.fetchall() or []
# Collect strategy IDs
strategy_ids = [s['id'] for s in strategies]
# Batch load positions for these strategies
positions_map = {}
if strategy_ids:
placeholders = ','.join(['?'] * len(strategy_ids))
cur.execute(
f"""
SELECT strategy_id, symbol, side, size, entry_price, current_price,
unrealized_pnl, pnl_percent, equity, updated_at
FROM qd_strategy_positions
WHERE strategy_id IN ({placeholders})
ORDER BY strategy_id, updated_at DESC
""",
tuple(strategy_ids)
)
for pos in (cur.fetchall() or []):
sid = pos['strategy_id']
if sid not in positions_map:
positions_map[sid] = []
positions_map[sid].append(dict(pos))
# Batch load recent trade stats (realized PnL per strategy)
trade_stats_map = {}
if strategy_ids:
placeholders = ','.join(['?'] * len(strategy_ids))
cur.execute(
f"""
SELECT strategy_id,
COUNT(*) as trade_count,
COALESCE(SUM(profit), 0) as total_realized_pnl
FROM qd_strategy_trades
WHERE strategy_id IN ({placeholders})
GROUP BY strategy_id
""",
tuple(strategy_ids)
)
for row in (cur.fetchall() or []):
trade_stats_map[row['strategy_id']] = {
'trade_count': row['trade_count'],
'total_realized_pnl': float(row['total_realized_pnl'] or 0)
}
cur.close()
# Build response
items = []
for s in strategies:
sid = s['id']
indicator_config = _safe_json_loads(s.get('indicator_config'), {})
trading_config = _safe_json_loads(s.get('trading_config'), {})
exchange_config = _safe_json_loads(s.get('exchange_config'), {})
# Extract indicator name
indicator_name = ''
if isinstance(indicator_config, dict):
indicator_name = indicator_config.get('indicator_name') or indicator_config.get('name') or ''
# Extract exchange name
exchange_name = ''
if isinstance(exchange_config, dict):
exchange_name = exchange_config.get('exchange_id') or exchange_config.get('exchange') or ''
# Positions data
positions = positions_map.get(sid, [])
total_unrealized_pnl = sum(float(p.get('unrealized_pnl') or 0) for p in positions)
total_equity = sum(float(p.get('equity') or 0) for p in positions)
position_count = len(positions)
# Trade stats
trade_stats = trade_stats_map.get(sid, {'trade_count': 0, 'total_realized_pnl': 0})
total_realized_pnl = trade_stats['total_realized_pnl']
trade_count = trade_stats['trade_count']
# Calculate total PnL and ROI
initial_capital = float(s.get('initial_capital') or 0)
total_pnl = total_unrealized_pnl + total_realized_pnl
roi = (total_pnl / initial_capital * 100) if initial_capital > 0 else 0
# Cross-sectional info
cs_type = ''
symbol_list = []
if isinstance(trading_config, dict):
cs_type = trading_config.get('cs_strategy_type') or 'single'
symbol_list = trading_config.get('symbol_list') or []
# Format timestamps
created_at = s.get('created_at')
updated_at = s.get('updated_at')
if hasattr(created_at, 'isoformat'):
created_at = created_at.isoformat()
if hasattr(updated_at, 'isoformat'):
updated_at = updated_at.isoformat()
# Format position timestamps
for p in positions:
if hasattr(p.get('updated_at'), 'isoformat'):
p['updated_at'] = p['updated_at'].isoformat()
items.append({
'id': sid,
'user_id': s['user_id'],
'username': s.get('username') or '',
'nickname': s.get('nickname') or '',
'strategy_name': s.get('strategy_name') or '',
'strategy_type': s.get('strategy_type') or '',
'cs_strategy_type': cs_type,
'market_category': s.get('market_category') or '',
'execution_mode': s.get('execution_mode') or '',
'status': s.get('status') or 'stopped',
'symbol': s.get('symbol') or '',
'symbol_list': symbol_list,
'timeframe': s.get('timeframe') or '',
'initial_capital': initial_capital,
'leverage': int(s.get('leverage') or 1),
'market_type': s.get('market_type') or '',
'indicator_name': indicator_name,
'exchange_name': exchange_name,
'decide_interval': s.get('decide_interval') or 300,
'position_count': position_count,
'total_unrealized_pnl': round(total_unrealized_pnl, 4),
'total_realized_pnl': round(total_realized_pnl, 4),
'total_pnl': round(total_pnl, 4),
'total_equity': round(total_equity, 4),
'roi': round(roi, 2),
'trade_count': trade_count,
'positions': positions,
'created_at': created_at,
'updated_at': updated_at
})
# Compute summary stats
all_running = [i for i in items if i['status'] == 'running']
total_capital = sum(i['initial_capital'] for i in items)
total_system_pnl = sum(i['total_pnl'] for i in items)
total_running = len(all_running)
return jsonify({
'code': 1,
'msg': 'success',
'data': {
'items': items,
'total': total,
'page': page,
'page_size': page_size,
'summary': {
'total_strategies': total,
'running_strategies': total_running,
'total_capital': round(total_capital, 2),
'total_pnl': round(total_system_pnl, 4),
'total_roi': round((total_system_pnl / total_capital * 100) if total_capital > 0 else 0, 2)
}
}
})
except Exception as e:
logger.error(f"get_system_strategies failed: {e}")
import traceback
logger.error(traceback.format_exc())
return jsonify({'code': 0, 'msg': str(e), 'data': None}), 500