""" 交易记录 API 路由(纯数据库版,事务安全) """ from flask import Blueprint, request, jsonify, session from psycopg2.extras import RealDictCursor from db import ( login_required, get_current_user_id, get_db, put_db, db_get_trades, db_get_available_cash, db_update_available_cash ) bp = Blueprint('trades', __name__, url_prefix='/api') def _parse_float(val): if val is None or val == '': return None try: return round(float(val), 4) except Exception: return None def _parse_int(val): if val is None or val == '': return None try: return int(val) except Exception: return None def _calc_cash_delta(trade_type, price, quantity): """计算交易对可用资金的影响""" if not trade_type or price is None or quantity is None: return 0 amount = round(float(price) * int(quantity), 2) t = trade_type.lower() if t == 'buy': return -amount elif t == 'sell': return amount return 0 @bp.route('/trades', methods=['GET']) @login_required def get_trades(): """获取交易记录""" user_id = get_current_user_id() trades = db_get_trades(user_id) return jsonify({'success': True, 'trades': trades}) @bp.route('/trades', methods=['POST']) @login_required def add_trade(): """添加交易记录(trade 插入 + 可用资金更新在同一事务中)""" conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: user_id = get_current_user_id() data = request.get_json() data['price'] = _parse_float(data.get('price')) data['quantity'] = _parse_int(data.get('quantity')) data['profit_amount'] = _parse_float(data.get('profit_amount')) data['stop_loss_price'] = _parse_float(data.get('stop_loss_price')) cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" INSERT INTO trades (user_id, stock_code, stock_name, trade_type, price, quantity, trade_date, reason, result, profit_amount, stop_loss_price, notes) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id, stock_code, stock_name, trade_type, price, quantity, trade_date::text, reason, result, profit_amount, stop_loss_price, notes, created_at::text """, (user_id, data.get('stock_code'), data.get('stock_name'), data.get('trade_type'), data.get('price'), data.get('quantity'), data.get('trade_date'), data.get('reason'), data.get('result'), data.get('profit_amount'), data.get('stop_loss_price'), data.get('notes'))) trade = cur.fetchone() delta = _calc_cash_delta(data.get('trade_type'), data.get('price'), data.get('quantity')) new_cash = None if delta != 0: cur.execute("SELECT available_cash FROM users WHERE id = %s FOR UPDATE", (user_id,)) row = cur.fetchone() current = float(row['available_cash'] or 0) if row else 0 new_cash = max(0, round(current + delta, 2)) cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (new_cash, user_id)) conn.commit() resp = {'success': True, 'trade': trade} if new_cash is not None: resp['available_cash'] = new_cash return jsonify(resp) except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 400 finally: put_db(conn) @bp.route('/trades/', methods=['PUT']) @login_required def update_trade(trade_id): """更新交易记录(同一事务内回滚旧资金 + 应用新资金)""" conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: user_id = get_current_user_id() data = request.get_json() if 'price' in data: data['price'] = _parse_float(data.get('price')) if 'quantity' in data: data['quantity'] = _parse_int(data.get('quantity')) if 'profit_amount' in data: data['profit_amount'] = _parse_float(data.get('profit_amount')) if 'stop_loss_price' in data: data['stop_loss_price'] = _parse_float(data.get('stop_loss_price')) cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" SELECT id, stock_code, stock_name, trade_type, price, quantity, trade_date::text, reason, result, profit_amount, stop_loss_price, notes FROM trades WHERE id = %s AND user_id = %s """, (trade_id, user_id)) old_trade = cur.fetchone() if not old_trade: put_db(conn) return jsonify({'error': '交易记录不存在'}), 404 cur.execute(""" UPDATE trades SET stock_code = COALESCE(%s, stock_code), stock_name = COALESCE(%s, stock_name), trade_type = COALESCE(%s, trade_type), price = COALESCE(%s, price), quantity = COALESCE(%s, quantity), trade_date = COALESCE(%s, trade_date), reason = COALESCE(%s, reason), result = COALESCE(%s, result), profit_amount = COALESCE(%s, profit_amount), stop_loss_price = COALESCE(%s, stop_loss_price), notes = COALESCE(%s, notes) WHERE id = %s AND user_id = %s RETURNING id, stock_code, stock_name, trade_type, price, quantity, trade_date::text, reason, result, profit_amount, stop_loss_price, notes, created_at::text """, (data.get('stock_code'), data.get('stock_name'), data.get('trade_type'), data.get('price'), data.get('quantity'), data.get('trade_date'), data.get('reason'), data.get('result'), data.get('profit_amount'), data.get('stop_loss_price'), data.get('notes'), trade_id, user_id)) trade = cur.fetchone() if not trade: conn.rollback() put_db(conn) return jsonify({'error': '交易记录不存在'}), 404 old_delta = _calc_cash_delta(old_trade.get('trade_type'), old_trade.get('price'), old_trade.get('quantity')) new_delta = _calc_cash_delta(trade.get('trade_type'), trade.get('price'), trade.get('quantity')) delta = new_delta - old_delta new_cash = None if delta != 0: cur.execute("SELECT available_cash FROM users WHERE id = %s FOR UPDATE", (user_id,)) row = cur.fetchone() current = float(row['available_cash'] or 0) if row else 0 new_cash = max(0, round(current + delta, 2)) cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (new_cash, user_id)) conn.commit() resp = {'success': True, 'trade': trade} if new_cash is not None: resp['available_cash'] = new_cash return jsonify(resp) except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 400 finally: put_db(conn) @bp.route('/trades/', methods=['DELETE']) @login_required def delete_trade(trade_id): """删除交易记录(同一事务内删除 + 回滚资金)""" conn = get_db() if not conn: return jsonify({'success': False, 'error': '数据库连接失败'}), 500 try: user_id = get_current_user_id() cur = conn.cursor(cursor_factory=RealDictCursor) cur.execute(""" SELECT trade_type, price, quantity FROM trades WHERE id = %s AND user_id = %s """, (trade_id, user_id)) old_trade = cur.fetchone() if not old_trade: put_db(conn) return jsonify({'success': False, 'error': '交易记录不存在'}), 404 cur.execute("DELETE FROM trades WHERE id = %s AND user_id = %s", (trade_id, user_id)) delta = -_calc_cash_delta(old_trade.get('trade_type'), old_trade.get('price'), old_trade.get('quantity')) new_cash = None if delta != 0: cur.execute("SELECT available_cash FROM users WHERE id = %s FOR UPDATE", (user_id,)) row = cur.fetchone() current = float(row['available_cash'] or 0) if row else 0 new_cash = max(0, round(current + delta, 2)) cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (new_cash, user_id)) conn.commit() resp = {'success': True} if new_cash is not None: resp['available_cash'] = new_cash return jsonify(resp) except Exception as e: conn.rollback() return jsonify({'success': False, 'error': str(e)}), 400 finally: put_db(conn) @bp.route('/available_cash', methods=['GET']) @login_required def get_available_cash(): """获取可用资金""" user_id = get_current_user_id() cash = db_get_available_cash(user_id) return jsonify({'success': True, 'available_cash': cash}) @bp.route('/available_cash', methods=['PUT']) @login_required def update_available_cash(): """更新可用资金""" try: user_id = get_current_user_id() data = request.get_json() amount = data.get('amount') if amount is None: return jsonify({'success': False, 'error': '金额不能为空'}), 400 try: amount = round(float(amount), 2) except: return jsonify({'success': False, 'error': '金额格式错误'}), 400 success, error = db_update_available_cash(user_id, amount) if error: return jsonify({'success': False, 'error': error}), 400 return jsonify({'success': True, 'available_cash': amount}) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 400 @bp.route('/stoploss_check', methods=['GET']) @login_required def check_stoploss(): """检查止损线触发情况""" try: user_id = get_current_user_id() trades = db_get_trades(user_id) # 计算每只股票的持仓情况 holdings = {} for trade in trades: code = trade.get('stock_code') if not code: continue if code not in holdings: holdings[code] = { 'code': code, 'name': trade.get('stock_name', code), 'total_cost': 0, 'total_quantity': 0, 'stop_loss_price': trade.get('stop_loss_price'), } trade_type = trade.get('trade_type') quantity = int(trade.get('quantity', 0) or 0) price = float(trade.get('price', 0) or 0) if trade_type == 'buy': holdings[code]['total_cost'] += price * quantity holdings[code]['total_quantity'] += quantity elif trade_type == 'sell': holdings[code]['total_quantity'] -= quantity if holdings[code]['total_quantity'] > 0: cost_per_share = holdings[code]['total_cost'] / (holdings[code]['total_quantity'] + quantity) holdings[code]['total_cost'] -= cost_per_share * quantity if trade.get('stop_loss_price'): holdings[code]['stop_loss_price'] = trade.get('stop_loss_price') # 只保留有持仓的股票 active_holdings = {k: v for k, v in holdings.items() if v['total_quantity'] > 0} # 获取实时价格并检查止损 alerts = [] try: for code, holding in active_holdings.items(): try: # 优先使用腾讯财经API(兼容腾讯云) current_price = 0 try: import requests as _rq _tc = ('sh' if code.startswith('6') else 'sz') + code _rr = _rq.get(f'http://qt.gtimg.cn/q={_tc}', timeout=5, headers={'Referer': 'https://finance.qq.com'}) if _rr.status_code == 200 and '\"' in _rr.text: _ff = _rr.text.split('\"')[1].split('~') if len(_ff) > 3 and _ff[3]: current_price = float(_ff[3]) except Exception: pass if current_price > 0: stop_loss_price = holding.get('stop_loss_price') avg_cost = float(holding['total_cost']) / holding['total_quantity'] if holding['total_quantity'] > 0 else 0.0 profit_loss = (current_price - avg_cost) * holding['total_quantity'] profit_percent = ((current_price - avg_cost) / avg_cost * 100) if avg_cost > 0 else 0 alert_data = { 'code': code, 'name': holding['name'], 'current_price': current_price, 'avg_cost': round(avg_cost, 4), 'quantity': holding['total_quantity'], 'profit_loss': round(profit_loss, 2), 'profit_percent': round(profit_percent, 2), 'stop_loss_price': stop_loss_price, 'triggered': False } if stop_loss_price and current_price <= stop_loss_price: alert_data['triggered'] = True alert_data['alert_type'] = 'stop_loss' alert_data['message'] = f"⚠️ {holding['name']} 触发止损!" elif profit_percent <= -5: alert_data['triggered'] = True alert_data['alert_type'] = 'default_stop' alert_data['message'] = f"⚠️ {holding['name']} 跌破成本5%!" alerts.append(alert_data) except Exception as e: print(f"获取 {code} 价格失败: {e}") except Exception as e: print(f"获取实时行情失败: {e}") return jsonify({ 'success': True, 'holdings': list(active_holdings.values()), 'alerts': [a for a in alerts if a.get('triggered')], 'all_positions': alerts }) except Exception as e: print(f"止损检查错误: {e}") return jsonify({'error': str(e)}), 500