""" 智能交易引擎 API 路由 提供算法配置管理、引擎状态查看、手动触发等功能 """ from flask import Blueprint, request, jsonify from datetime import date from db import get_db, login_required, get_current_user_id from psycopg2.extras import RealDictCursor bp = Blueprint('smart_trade', __name__, url_prefix='/api/smart') # ═══════════════════════════════════════════════════════ # 1. 算法模板 # ═══════════════════════════════════════════════════════ @bp.route('/templates', methods=['GET']) @login_required def get_algo_templates(): """获取所有预置算法模板""" conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" SELECT id, name, display_name, description, risk_level, take_profit_pct::float, stop_loss_pct::float, ignore_sell_signal, sell_confirm_days, max_hold_days, no_timeout_if_rising, position_pct::float, signal_weight, partial_exit_pct, momentum_trail_gap::float, breakeven_at::float, momentum_tp, momentum_days, buy_time, sell_time, backtest_annual_return::float, backtest_max_drawdown::float, backtest_win_rate::float, backtest_calmar::float FROM algo_templates ORDER BY backtest_calmar DESC NULLS LAST """) templates = cur.fetchall() return jsonify({'success': True, 'templates': templates}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() # ═══════════════════════════════════════════════════════ # 2. 用户算法配置 (CRUD) # ═══════════════════════════════════════════════════════ @bp.route('/config', methods=['GET']) @login_required def get_algo_config(): """获取用户当前的算法配置""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: from services.smart_trade_engine import get_user_algo_config, DEFAULT_CONFIG config = get_user_algo_config(conn, user_id) # 判断是否是默认配置(没有存入数据库) cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute("SELECT COUNT(*) as cnt FROM sim_algo_config WHERE user_id = %s", (user_id,)) has_config = cur.fetchone()['cnt'] > 0 return jsonify({ 'success': True, 'config': config, 'is_default': not has_config, }) except Exception as e: import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() @bp.route('/config', methods=['POST']) @login_required def save_algo_config(): """保存/更新用户的算法配置""" user_id = get_current_user_id() data = request.get_json() if not data: return jsonify({'success': False, 'error': '无效参数'}), 400 conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: from services.smart_trade_engine import save_user_algo_config, DEFAULT_CONFIG # 合并默认值 config = dict(DEFAULT_CONFIG) for key in config: if key in data: config[key] = data[key] save_user_algo_config(conn, user_id, config) return jsonify({'success': True, 'message': '算法配置已保存'}) except Exception as e: conn.rollback() import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() @bp.route('/apply_template', methods=['POST']) @login_required def apply_template(): """从模板应用算法配置""" user_id = get_current_user_id() data = request.get_json() template_name = data.get('template_name') if not template_name: return jsonify({'success': False, 'error': '缺少template_name'}), 400 conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: from services.smart_trade_engine import apply_template as do_apply ok = do_apply(conn, user_id, template_name) if ok: return jsonify({'success': True, 'message': f'已应用模板: {template_name}'}) else: return jsonify({'success': False, 'error': f'模板不存在: {template_name}'}), 404 except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() # ═══════════════════════════════════════════════════════ # 3. 引擎状态 # ═══════════════════════════════════════════════════════ @bp.route('/status', methods=['GET']) @login_required def get_status(): """获取智能交易引擎的当前状态(含持仓详情+活跃规则+信号日志)""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: from services.smart_trade_engine import get_engine_status status = get_engine_status(conn, user_id) return jsonify({'success': True, **status}) except Exception as e: import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() # ═══════════════════════════════════════════════════════ # 4. 手动触发 # ═══════════════════════════════════════════════════════ @bp.route('/trigger', methods=['POST']) @login_required def trigger_smart_trade(): """手动触发智能交易引擎执行""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: from services.smart_trade_engine import execute_smart_trade result = execute_smart_trade(conn, user_id, scan_date=None) if result.get('error') and not result.get('success'): return jsonify({'success': False, 'error': result['error']}), 500 return jsonify({ 'success': True, 'results': result.get('results', []), 'signals': result.get('signals', 0), 'algo': result.get('algo', 'unknown'), 'total_fees': result.get('total_fees', 0), 'detail_reasons': result.get('detail_reasons', []), 'skipped_limit': result.get('skipped_limit', []), 'skipped_t1': result.get('skipped_t1', []), 'available_cash': result.get('available_cash', 0), 'message': f"智能引擎执行完成: {result.get('signals', 0)}笔信号" }) except Exception as e: import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() # ═══════════════════════════════════════════════════════ # 5. 信号日志 # ═══════════════════════════════════════════════════════ @bp.route('/signals', methods=['GET']) @login_required def get_signals(): """获取交易信号日志""" user_id = get_current_user_id() limit = request.args.get('limit', 50, type=int) days = request.args.get('days', 7, 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, signal_date::text, signal_time::text, stock_code, stock_name, action, reason, algo_rule, signal_price::float, buy_price::float, profit_pct::float, executed, execute_price::float, execute_shares, created_at::text FROM sim_trade_signals WHERE user_id = %s AND signal_date >= CURRENT_DATE - %s ORDER BY signal_date DESC, id DESC LIMIT %s """, (user_id, days, limit)) signals = cur.fetchall() return jsonify({'success': True, 'signals': signals}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close() # ═══════════════════════════════════════════════════════ # 6. 持仓元数据(前端持仓详情扩展) # ═══════════════════════════════════════════════════════ @bp.route('/position_meta', methods=['GET']) @login_required def get_position_meta(): """获取持仓的算法元数据(止盈止损状态等)""" user_id = get_current_user_id() conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: from services.smart_trade_engine import get_all_position_meta positions = get_all_position_meta(conn, user_id) result = [] for p in positions: result.append({ 'stock_code': p['stock_code'], 'buy_date': str(p.get('buy_date', '')), 'buy_price': float(p.get('buy_price', 0)), 'days_held': p.get('days_held', 0), 'max_price': float(p.get('max_price_since_buy', 0) or 0), 'consecutive_up_days': p.get('consecutive_up_days', 0), 'consecutive_sell_signals': p.get('consecutive_sell_signals', 0), 'partial_exit_done': p.get('partial_exit_done', False), 'breakeven_active': p.get('breakeven_active', False), 'momentum_trailing_active': p.get('momentum_trailing_active', False), 'momentum_high_price': float(p.get('momentum_high_price', 0) or 0), 'current_shares': p.get('current_shares', 0), 'original_shares': p.get('original_shares', 0), }) return jsonify({'success': True, 'positions': result}) except Exception as e: import traceback traceback.print_exc() return jsonify({'success': False, 'error': str(e)}), 500 finally: conn.close()