Files
stock/stock-html/routes/trades.py
T
freedakgmail 5f5fa7ae80 fix: 修复连接泄漏、double-put_db、北交所映射缺失
- analysis.py: 6个端点将 put_db(conn) 从 try 移到 finally,异常时不泄漏
- trades.py: 移除 update_trade/delete_trade 中多余的 put_db(conn),避免 double-put 破坏连接池
- trades.py: stoploss_check 补充北交所(8/9开头)腾讯API代码映射
2026-07-22 07:31:21 +08:00

359 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
交易记录 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/<int:trade_id>', 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:
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()
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/<int:trade_id>', 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:
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 'bj' if code.startswith(('8', '9')) 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