diff --git a/backend_api_python/app/routes/user.py b/backend_api_python/app/routes/user.py index 5301f3e..88edd62 100644 --- a/backend_api_python/app/routes/user.py +++ b/backend_api_python/app/routes/user.py @@ -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 diff --git a/quantdinger_vue/src/api/user.js b/quantdinger_vue/src/api/user.js index 73cfc9f..f8e9979 100644 --- a/quantdinger_vue/src/api/user.js +++ b/quantdinger_vue/src/api/user.js @@ -208,3 +208,15 @@ export function getUserCreditsLog (params) { params }) } + +/** + * Get system-wide strategy overview (admin only) + * @param {Object} params - { page, page_size, status, search } + */ +export function getSystemStrategies (params) { + return request({ + url: '/api/users/system-strategies', + method: 'get', + params + }) +} diff --git a/quantdinger_vue/src/locales/lang/en-US.js b/quantdinger_vue/src/locales/lang/en-US.js index 46fe4a9..af0eeae 100644 --- a/quantdinger_vue/src/locales/lang/en-US.js +++ b/quantdinger_vue/src/locales/lang/en-US.js @@ -2561,6 +2561,36 @@ const locale = { 'userManage.selectDate': 'Please select a date', 'userManage.remark': 'Remark', 'userManage.remarkPlaceholder': 'Optional remark', + 'userManage.tabUsers': 'User Management', + + // System Overview (Admin) + 'systemOverview.tabTitle': 'System Overview', + 'systemOverview.totalStrategies': 'Total Strategies', + 'systemOverview.runningStrategies': 'Running', + 'systemOverview.totalCapital': 'Total Capital', + 'systemOverview.totalPnl': 'Total PnL', + 'systemOverview.filterAll': 'All Status', + 'systemOverview.filterRunning': 'Running', + 'systemOverview.filterStopped': 'Stopped', + 'systemOverview.searchPlaceholder': 'Search strategy/symbol/user', + 'systemOverview.running': 'Running', + 'systemOverview.stopped': 'Stopped', + 'systemOverview.colUser': 'User', + 'systemOverview.colStrategy': 'Strategy', + 'systemOverview.colStatus': 'Status', + 'systemOverview.colSymbol': 'Symbol', + 'systemOverview.colCapital': 'Capital', + 'systemOverview.colPnl': 'PnL / ROI', + 'systemOverview.colPositions': 'Pos', + 'systemOverview.colTrades': 'Trades', + 'systemOverview.colIndicator': 'Indicator', + 'systemOverview.colExchange': 'Exchange', + 'systemOverview.colTimeframe': 'TF', + 'systemOverview.colLeverage': 'Lev', + 'systemOverview.colCreatedAt': 'Created', + 'systemOverview.realized': 'Real', + 'systemOverview.unrealized': 'Unreal', + 'systemOverview.symbols': 'symbols', // Settings - Billing 'settings.group.billing': 'Billing & Credits', diff --git a/quantdinger_vue/src/locales/lang/zh-CN.js b/quantdinger_vue/src/locales/lang/zh-CN.js index a8aa5dc..48cf2b1 100644 --- a/quantdinger_vue/src/locales/lang/zh-CN.js +++ b/quantdinger_vue/src/locales/lang/zh-CN.js @@ -2370,6 +2370,36 @@ const locale = { 'userManage.selectDate': '请选择日期', 'userManage.remark': '备注', 'userManage.remarkPlaceholder': '可选备注', + 'userManage.tabUsers': '用户管理', + + // System Overview (Admin) + 'systemOverview.tabTitle': '系统总览', + 'systemOverview.totalStrategies': '策略总数', + 'systemOverview.runningStrategies': '运行中', + 'systemOverview.totalCapital': '总资金', + 'systemOverview.totalPnl': '总盈亏', + 'systemOverview.filterAll': '全部状态', + 'systemOverview.filterRunning': '运行中', + 'systemOverview.filterStopped': '已停止', + 'systemOverview.searchPlaceholder': '搜索策略名/交易对/用户名', + 'systemOverview.running': '运行中', + 'systemOverview.stopped': '已停止', + 'systemOverview.colUser': '用户', + 'systemOverview.colStrategy': '策略名称', + 'systemOverview.colStatus': '状态', + 'systemOverview.colSymbol': '交易对', + 'systemOverview.colCapital': '资金', + 'systemOverview.colPnl': '盈亏 / ROI', + 'systemOverview.colPositions': '持仓', + 'systemOverview.colTrades': '交易次数', + 'systemOverview.colIndicator': '指标', + 'systemOverview.colExchange': '交易所', + 'systemOverview.colTimeframe': '周期', + 'systemOverview.colLeverage': '杠杆', + 'systemOverview.colCreatedAt': '创建时间', + 'systemOverview.realized': '已实现', + 'systemOverview.unrealized': '未实现', + 'systemOverview.symbols': '个交易对', // Settings - Billing 'settings.group.billing': '计费配置', diff --git a/quantdinger_vue/src/views/user-manage/index.vue b/quantdinger_vue/src/views/user-manage/index.vue index 5172bb6..5bb3a7b 100644 --- a/quantdinger_vue/src/views/user-manage/index.vue +++ b/quantdinger_vue/src/views/user-manage/index.vue @@ -8,113 +8,289 @@

{{ $t('userManage.description') || 'Manage system users, roles and permissions' }}

- -
-
- - - {{ $t('userManage.createUser') || 'Create User' }} - - - - {{ $t('common.refresh') || 'Refresh' }} - -
-
- -
-
+ + + + + +
+
+ + + {{ $t('userManage.createUser') || 'Create User' }} + + + + {{ $t('common.refresh') || 'Refresh' }} + +
+
+ +
+
- - - - - + + + + + - - + + - - + + - - + + - - + + - - - - + + + + +
+ + + + +
+
+
+ +
+
+
{{ strategySummary.total_strategies || 0 }}
+
{{ $t('systemOverview.totalStrategies') || 'Total Strategies' }}
+
+
+
+
+ +
+
+
{{ strategySummary.running_strategies || 0 }}
+
{{ $t('systemOverview.runningStrategies') || 'Running' }}
+
+
+
+
+ +
+
+
{{ formatNumber(strategySummary.total_capital) }}
+
{{ $t('systemOverview.totalCapital') || 'Total Capital' }}
+
+
+
+
+ +
+
+
+ {{ formatPnl(strategySummary.total_pnl) }} + {{ strategySummary.total_roi || 0 }}% +
+
{{ $t('systemOverview.totalPnl') || 'Total PnL' }}
+
+
+
+ + +
+
+ + + {{ $t('common.refresh') || 'Refresh' }} + + + {{ $t('systemOverview.filterAll') || 'All Status' }} + {{ $t('systemOverview.filterRunning') || 'Running' }} + {{ $t('systemOverview.filterStopped') || 'Stopped' }} + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+