""" 模拟交易 API 路由 每个交易日10点根据推荐率最高的买入卖出方案进行自动操作 """ from flask import Blueprint, request, jsonify from datetime import datetime, date, time from db import get_db, put_db, login_required, get_current_user_id from psycopg2.extras import RealDictCursor bp = Blueprint('sim_trade', __name__, url_prefix='/api/sim') def init_user_config(user_id): """初始化用户模拟交易配置""" conn = get_db() if not conn: return None try: cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" INSERT INTO sim_config (user_id) VALUES (%s) ON CONFLICT (user_id) DO NOTHING RETURNING * """, (user_id,)) conn.commit() # 获取配置 cur.execute("SELECT * FROM sim_config WHERE user_id = %s", (user_id,)) return cur.fetchone() finally: put_db(conn) @bp.route('/config', methods=['GET']) @login_required def get_config(): """获取模拟交易配置""" user_id = get_current_user_id() config = init_user_config(user_id) if config: return jsonify({ 'success': True, 'config': { 'initial_capital': float(config['initial_capital']), 'trade_quantity': config['trade_quantity'], 'auto_trade_enabled': config['auto_trade_enabled'], 'auto_trade_time': str(config['auto_trade_time']) if config['auto_trade_time'] else '10:00:00' } }) return jsonify({'success': False, 'error': '获取配置失败'}), 500 @bp.route('/config', methods=['POST']) @login_required def update_config(): """更新模拟交易配置""" user_id = get_current_user_id() data = request.get_json() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor() cur.execute(""" UPDATE sim_config SET trade_quantity = COALESCE(%s, trade_quantity), auto_trade_enabled = COALESCE(%s, auto_trade_enabled), updated_at = NOW() WHERE user_id = %s """, ( data.get('trade_quantity'), data.get('auto_trade_enabled'), user_id )) conn.commit() return jsonify({'success': True}) except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 500 finally: put_db(conn) @bp.route('/trades', methods=['GET']) @login_required def get_trades(): """获取模拟交易记录""" user_id = get_current_user_id() limit = request.args.get('limit', 100, type=int) conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" SELECT id, stock_code, stock_name, trade_type, price::float, quantity, trade_date::text, trade_time::text, recommend_rate::float, signal_reason, COALESCE(commission, 0)::float as commission, COALESCE(stamp_tax, 0)::float as stamp_tax, COALESCE(total_fee, 0)::float as total_fee, created_at::text FROM sim_trades WHERE user_id = %s ORDER BY trade_date DESC, trade_time DESC LIMIT %s """, (user_id, limit)) trades = cur.fetchall() return jsonify({'success': True, 'trades': trades}) finally: put_db(conn) @bp.route('/positions', methods=['GET']) @login_required def get_positions(): """获取模拟持仓""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" SELECT stock_code, stock_name, quantity, avg_cost::float, total_cost::float, current_price::float, updated_at::text FROM sim_positions WHERE user_id = %s AND quantity > 0 ORDER BY total_cost DESC """, (user_id,)) positions = cur.fetchall() return jsonify({'success': True, 'positions': positions}) finally: put_db(conn) @bp.route('/stats', methods=['GET']) @login_required def get_stats(): """获取模拟交易统计""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) # 获取配置 — 优先使用 sim_algo_config.total_capital(智能交易配置) initial_capital = 200000 # 默认值 cur.execute("SELECT total_capital::float FROM sim_algo_config WHERE user_id = %s AND is_active = TRUE", (user_id,)) algo_cfg = cur.fetchone() if algo_cfg and algo_cfg['total_capital']: initial_capital = algo_cfg['total_capital'] else: cur.execute("SELECT initial_capital::float FROM sim_config WHERE user_id = %s", (user_id,)) config = cur.fetchone() if config and config['initial_capital']: initial_capital = config['initial_capital'] # 获取持仓统计 cur.execute(""" SELECT COALESCE(SUM(quantity * current_price), 0)::float as total_market_value, COALESCE(SUM(total_cost), 0)::float as total_cost, COALESCE(SUM(quantity * current_price - total_cost), 0)::float as unrealized_profit FROM sim_positions WHERE user_id = %s AND quantity > 0 """, (user_id,)) position_stats = cur.fetchone() # 获取已实现盈亏(卖出交易) cur.execute(""" SELECT COALESCE(SUM( CASE WHEN trade_type = 'sell' THEN price * quantity ELSE 0 END ), 0)::float as total_sell, COUNT(DISTINCT trade_date) as trade_days, COUNT(*) as total_trades, COALESCE(SUM(COALESCE(commission, 0)), 0)::float as total_commission, COALESCE(SUM(COALESCE(stamp_tax, 0)), 0)::float as total_stamp_tax, COALESCE(SUM(COALESCE(total_fee, 0)), 0)::float as total_fees FROM sim_trades WHERE user_id = %s """, (user_id,)) trade_stats = cur.fetchone() # 计算已实现盈亏(需要更复杂的计算,这里简化处理) # 从每日统计表获取最新的已实现盈亏 cur.execute(""" SELECT realized_profit::float FROM sim_daily_stats WHERE user_id = %s ORDER BY stat_date DESC LIMIT 1 """, (user_id,)) daily_stat = cur.fetchone() realized_profit = daily_stat['realized_profit'] if daily_stat else 0 # 获取历史统计(用于图表) cur.execute(""" SELECT stat_date::text, total_profit::float, total_market_value::float, realized_profit::float FROM sim_daily_stats WHERE user_id = %s ORDER BY stat_date DESC LIMIT 30 """, (user_id,)) history = cur.fetchall() total_market_value = position_stats['total_market_value'] or 0 total_cost = position_stats['total_cost'] or 0 unrealized_profit = position_stats['unrealized_profit'] or 0 total_fees = trade_stats['total_fees'] or 0 total_commission = trade_stats['total_commission'] or 0 total_stamp_tax = trade_stats['total_stamp_tax'] or 0 # 毛利润(不含手续费的计算) gross_profit = unrealized_profit + realized_profit + total_fees # 加回手续费 = 毛收益 # 净利润(含手续费) net_profit = unrealized_profit + realized_profit # realized_profit 已扣除卖出手续费 total_profit = net_profit # 计算收益率 gross_rate = (gross_profit / initial_capital * 100) if initial_capital > 0 else 0 net_rate = (net_profit / initial_capital * 100) if initial_capital > 0 else 0 profit_rate = net_rate # 默认显示净收益率 return jsonify({ 'success': True, 'stats': { 'initial_capital': initial_capital, 'total_market_value': total_market_value, 'total_cost': total_cost, 'cash': initial_capital - total_cost + realized_profit, 'unrealized_profit': unrealized_profit, 'realized_profit': realized_profit, 'total_profit': total_profit, 'profit_rate': profit_rate, 'trade_days': trade_stats['trade_days'] or 0, 'total_trades': trade_stats['total_trades'] or 0, # 手续费明细 'total_fees': total_fees, 'total_commission': total_commission, 'total_stamp_tax': total_stamp_tax, # 对比数据: 毛收益 vs 净收益 'gross_profit': gross_profit, 'gross_rate': gross_rate, 'net_profit': net_profit, 'net_rate': net_rate, }, 'history': list(reversed(history)) if history else [] }) finally: put_db(conn) @bp.route('/execute', methods=['POST']) @login_required def execute_trade(): """执行模拟交易(手动或自动)""" user_id = get_current_user_id() data = request.get_json() stock_code = data.get('stock_code') stock_name = data.get('stock_name', '') trade_type = data.get('trade_type') # 'buy' or 'sell' price = data.get('price') quantity = data.get('quantity', 1000) recommend_rate = data.get('recommend_rate') signal_reason = data.get('signal_reason', '') if not stock_code or not trade_type or not price: return jsonify({'success': False, 'error': '参数不完整'}), 400 conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) today = date.today() now = datetime.now().time() # 1. 记录交易 cur.execute(""" INSERT INTO sim_trades (user_id, stock_code, stock_name, trade_type, price, quantity, trade_date, trade_time, recommend_rate, signal_reason) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """, (user_id, stock_code, stock_name, trade_type, price, quantity, today, now, recommend_rate, signal_reason)) trade_id = cur.fetchone()['id'] # 2. 更新持仓 if trade_type == 'buy': # 买入:增加持仓 cur.execute(""" INSERT INTO sim_positions (user_id, stock_code, stock_name, quantity, avg_cost, total_cost, current_price) VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (user_id, stock_code) DO UPDATE SET quantity = sim_positions.quantity + EXCLUDED.quantity, total_cost = sim_positions.total_cost + EXCLUDED.total_cost, avg_cost = (sim_positions.total_cost + EXCLUDED.total_cost) / (sim_positions.quantity + EXCLUDED.quantity), current_price = EXCLUDED.current_price, stock_name = COALESCE(EXCLUDED.stock_name, sim_positions.stock_name), updated_at = NOW() """, (user_id, stock_code, stock_name, quantity, price, price * quantity, price)) else: # 卖出:减少持仓,计算已实现盈亏 cur.execute(""" SELECT quantity, avg_cost::float, total_cost::float FROM sim_positions WHERE user_id = %s AND stock_code = %s """, (user_id, stock_code)) position = cur.fetchone() if not position or position['quantity'] < quantity: conn.rollback() return jsonify({'success': False, 'error': '持仓不足'}), 400 # 计算已实现盈亏 avg_cost = position['avg_cost'] realized_pnl = (price - avg_cost) * quantity # 更新持仓 new_quantity = position['quantity'] - quantity new_total_cost = position['total_cost'] - (avg_cost * quantity) if new_quantity > 0: cur.execute(""" UPDATE sim_positions SET quantity = %s, total_cost = %s, current_price = %s, updated_at = NOW() WHERE user_id = %s AND stock_code = %s """, (new_quantity, new_total_cost, price, user_id, stock_code)) else: # 清仓 cur.execute(""" UPDATE sim_positions SET quantity = 0, total_cost = 0, current_price = %s, updated_at = NOW() WHERE user_id = %s AND stock_code = %s """, (price, user_id, stock_code)) # 更新每日统计中的已实现盈亏 cur.execute(""" INSERT INTO sim_daily_stats (user_id, stat_date, realized_profit, trade_count) VALUES (%s, %s, %s, 1) ON CONFLICT (user_id, stat_date) DO UPDATE SET realized_profit = sim_daily_stats.realized_profit + %s, trade_count = sim_daily_stats.trade_count + 1 """, (user_id, today, realized_pnl, realized_pnl)) conn.commit() return jsonify({ 'success': True, 'trade_id': trade_id, 'message': f'{"买入" if trade_type == "buy" else "卖出"} {stock_name or stock_code} {quantity}股 成功' }) except Exception as e: conn.rollback() import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: put_db(conn) @bp.route('/auto_execute', methods=['POST']) @login_required def auto_execute(): """根据推荐率自动执行交易(每日10点调用)""" user_id = get_current_user_id() data = request.get_json() # 获取推荐的买入和卖出信号 buy_signals = data.get('buy_signals', []) # 按推荐率排序的买入信号 sell_signals = data.get('sell_signals', []) # 按推荐率排序的卖出信号 conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) # 获取配置 cur.execute("SELECT trade_quantity FROM sim_config WHERE user_id = %s", (user_id,)) config = cur.fetchone() quantity = config['trade_quantity'] if config else 1000 today = date.today() now = datetime.now().time() results = [] # 1. 先处理卖出信号(释放资金) for signal in sell_signals: stock_code = signal.get('code') # 检查是否有持仓 cur.execute(""" SELECT quantity FROM sim_positions WHERE user_id = %s AND stock_code = %s AND quantity > 0 """, (user_id, stock_code)) position = cur.fetchone() if position and position['quantity'] >= quantity: # 执行卖出 price = signal.get('price', 0) if price > 0: # 获取持仓成本 cur.execute(""" SELECT avg_cost::float FROM sim_positions WHERE user_id = %s AND stock_code = %s """, (user_id, stock_code)) pos_info = cur.fetchone() avg_cost = pos_info['avg_cost'] if pos_info else price realized_pnl = (price - avg_cost) * quantity # 记录交易 cur.execute(""" INSERT INTO sim_trades (user_id, stock_code, stock_name, trade_type, price, quantity, trade_date, trade_time, recommend_rate, signal_reason) VALUES (%s, %s, %s, 'sell', %s, %s, %s, %s, %s, %s) """, (user_id, stock_code, signal.get('name', ''), price, quantity, today, now, signal.get('recommendRate'), signal.get('reason', ''))) # 更新持仓 cur.execute(""" UPDATE sim_positions SET quantity = quantity - %s, total_cost = total_cost - (avg_cost * %s), current_price = %s, updated_at = NOW() WHERE user_id = %s AND stock_code = %s """, (quantity, quantity, price, user_id, stock_code)) # 更新已实现盈亏 cur.execute(""" INSERT INTO sim_daily_stats (user_id, stat_date, realized_profit, trade_count) VALUES (%s, %s, %s, 1) ON CONFLICT (user_id, stat_date) DO UPDATE SET realized_profit = sim_daily_stats.realized_profit + %s, trade_count = sim_daily_stats.trade_count + 1 """, (user_id, today, realized_pnl, realized_pnl)) results.append({ 'type': 'sell', 'code': stock_code, 'name': signal.get('name', ''), 'price': price, 'quantity': quantity, 'pnl': realized_pnl }) # 2. 处理买入信号(取推荐率最高的) for signal in buy_signals[:3]: # 最多买入3只 stock_code = signal.get('code') price = signal.get('price', 0) if price > 0: # 检查今日是否已买入该股票 cur.execute(""" SELECT COUNT(*) as cnt FROM sim_trades WHERE user_id = %s AND stock_code = %s AND trade_date = %s AND trade_type = 'buy' """, (user_id, stock_code, today)) if cur.fetchone()['cnt'] > 0: continue # 今日已买入,跳过 # 记录交易 cur.execute(""" INSERT INTO sim_trades (user_id, stock_code, stock_name, trade_type, price, quantity, trade_date, trade_time, recommend_rate, signal_reason) VALUES (%s, %s, %s, 'buy', %s, %s, %s, %s, %s, %s) """, (user_id, stock_code, signal.get('name', ''), price, quantity, today, now, signal.get('recommendRate'), signal.get('reason', ''))) # 更新持仓 cur.execute(""" INSERT INTO sim_positions (user_id, stock_code, stock_name, quantity, avg_cost, total_cost, current_price) VALUES (%s, %s, %s, %s, %s, %s, %s) ON CONFLICT (user_id, stock_code) DO UPDATE SET quantity = sim_positions.quantity + EXCLUDED.quantity, total_cost = sim_positions.total_cost + EXCLUDED.total_cost, avg_cost = (sim_positions.total_cost + EXCLUDED.total_cost) / (sim_positions.quantity + EXCLUDED.quantity), current_price = EXCLUDED.current_price, stock_name = COALESCE(EXCLUDED.stock_name, sim_positions.stock_name), updated_at = NOW() """, (user_id, stock_code, signal.get('name', ''), quantity, price, price * quantity, price)) results.append({ 'type': 'buy', 'code': stock_code, 'name': signal.get('name', ''), 'price': price, 'quantity': quantity }) conn.commit() return jsonify({ 'success': True, 'results': results, 'message': f'自动交易完成: 买入{len([r for r in results if r["type"]=="buy"])}笔, 卖出{len([r for r in results if r["type"]=="sell"])}笔' }) except Exception as e: conn.rollback() import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: put_db(conn) @bp.route('/update_prices', methods=['POST']) @login_required def update_prices(): """更新持仓的当前价格""" user_id = get_current_user_id() data = request.get_json() prices = data.get('prices', {}) # {stock_code: price} if not prices: return jsonify({'success': True, 'message': '无需更新'}) conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor() for code, price in prices.items(): cur.execute(""" UPDATE sim_positions SET current_price = %s, updated_at = NOW() WHERE user_id = %s AND stock_code = %s """, (price, user_id, code)) # 更新每日统计 today = date.today() cur.execute(""" SELECT COALESCE(SUM(quantity * current_price), 0) as market_value, COALESCE(SUM(total_cost), 0) as total_cost, COALESCE(SUM(quantity * current_price - total_cost), 0) as unrealized FROM sim_positions WHERE user_id = %s AND quantity > 0 """, (user_id,)) stats = cur.fetchone() cur.execute(""" INSERT INTO sim_daily_stats (user_id, stat_date, total_market_value, total_cost, unrealized_profit) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (user_id, stat_date) DO UPDATE SET total_market_value = EXCLUDED.total_market_value, total_cost = EXCLUDED.total_cost, unrealized_profit = EXCLUDED.unrealized_profit """, (user_id, today, stats[0], stats[1], stats[2])) conn.commit() return jsonify({'success': True}) except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 500 finally: put_db(conn) @bp.route('/reset', methods=['POST']) @login_required def reset_simulation(): """重置模拟交易(清空所有数据)""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor() cur.execute("DELETE FROM sim_trades WHERE user_id = %s", (user_id,)) cur.execute("DELETE FROM sim_positions WHERE user_id = %s", (user_id,)) cur.execute("DELETE FROM sim_daily_stats WHERE user_id = %s", (user_id,)) conn.commit() return jsonify({'success': True, 'message': '模拟交易已重置'}) except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 500 finally: put_db(conn) @bp.route('/trigger_trade', methods=['POST']) @login_required def trigger_trade(): """手动触发交易 — 优先使用智能引擎,降级到旧引擎""" user_id = get_current_user_id() try: # 优先使用智能引擎 smart_conn = None try: from services.smart_trade_engine import execute_smart_trade smart_conn = get_db() if smart_conn: result = execute_smart_trade(smart_conn, user_id, scan_date=None) if result.get('success'): results = result.get('results', []) buy_count = len([r for r in results if r['type'] == 'buy']) sell_count = len([r for r in results if r['type'] in ('sell', 'partial_sell')]) return jsonify({ 'success': True, 'results': results, 'algo': result.get('algo', 'unknown'), 'message': f"智能引擎[{result.get('algo','?')}]: 买入{buy_count}笔, 卖出{sell_count}笔" }) except Exception as e: print(f"[trigger_trade] 智能引擎异常,降级: {e}") finally: if smart_conn: put_db(smart_conn) # 降级: 使用旧引擎 from services.scheduler import execute_auto_trade_for_user conn = None try: conn = get_db() if conn: cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute("SELECT trade_quantity FROM sim_config WHERE user_id = %s", (user_id,)) config = cur.fetchone() trade_quantity = config['trade_quantity'] if config else 1000 else: trade_quantity = 1000 except Exception as e: print(f"[trigger_trade] 获取交易配置失败: {e}") trade_quantity = 1000 finally: if conn: put_db(conn) result = execute_auto_trade_for_user(user_id, trade_quantity) if 'error' in result: return jsonify({'success': False, 'error': result['error']}), 500 return jsonify({ 'success': True, 'results': result.get('results', []), 'message': f"自动交易完成: 买入{len([r for r in result.get('results', []) if r['type']=='buy'])}笔, 卖出{len([r for r in result.get('results', []) if r['type']=='sell'])}笔" }) except Exception as e: import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 @bp.route('/trigger_update', methods=['POST']) @login_required def trigger_update(): """手动触发价格更新(用于测试)""" user_id = get_current_user_id() try: from services.scheduler import update_positions_price_for_user update_positions_price_for_user(user_id) return jsonify({'success': True, 'message': '持仓价格已更新'}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @bp.route('/today_trades', methods=['GET']) @login_required def get_today_trades(): """获取今日交易记录""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) today = date.today() cur.execute(""" SELECT id, stock_code, stock_name, trade_type, price::float, quantity, trade_date::text, trade_time::text, recommend_rate::float, signal_reason, COALESCE(commission, 0)::float as commission, COALESCE(stamp_tax, 0)::float as stamp_tax, COALESCE(total_fee, 0)::float as total_fee, created_at::text FROM sim_trades WHERE user_id = %s AND trade_date = %s ORDER BY trade_time DESC """, (user_id, today)) trades = cur.fetchall() # 计算今日盈亏 cur.execute(""" SELECT realized_profit::float, trade_count FROM sim_daily_stats WHERE user_id = %s AND stat_date = %s """, (user_id, today)) stats = cur.fetchone() return jsonify({ 'success': True, 'trades': trades, 'today_stats': { 'realized_profit': stats['realized_profit'] if stats else 0, 'trade_count': stats['trade_count'] if stats else 0 } }) finally: put_db(conn)