314 lines
12 KiB
Python
314 lines
12 KiB
Python
"""
|
||
交易记录 API 路由(纯数据库版)
|
||
"""
|
||
from flask import Blueprint, request, jsonify, session
|
||
from db import (
|
||
login_required, get_current_user_id,
|
||
db_get_trades, db_get_trade, db_add_trade, db_update_trade, db_delete_trade,
|
||
db_get_available_cash, db_update_available_cash
|
||
)
|
||
|
||
bp = Blueprint('trades', __name__, url_prefix='/api')
|
||
|
||
|
||
@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():
|
||
"""添加交易记录"""
|
||
try:
|
||
user_id = get_current_user_id()
|
||
data = request.get_json()
|
||
|
||
# 处理数值(空字符串转为None)
|
||
def parse_float(val):
|
||
if val is None or val == '':
|
||
return None
|
||
try:
|
||
return round(float(val), 4)
|
||
except:
|
||
return None
|
||
|
||
def parse_int(val):
|
||
if val is None or val == '':
|
||
return None
|
||
try:
|
||
return int(val)
|
||
except:
|
||
return None
|
||
|
||
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'))
|
||
|
||
trade, error = db_add_trade(user_id, data)
|
||
if error:
|
||
return jsonify({'success': False, 'error': error}), 400
|
||
|
||
# 根据交易类型自动更新可用资金
|
||
trade_type = (data.get('trade_type') or '').lower()
|
||
price = data.get('price')
|
||
quantity = data.get('quantity')
|
||
if trade_type in ('buy', 'sell') and price is not None and quantity is not None:
|
||
amount = round(float(price) * int(quantity), 2)
|
||
current = db_get_available_cash(user_id)
|
||
if trade_type == 'buy':
|
||
new_cash = round(current - amount, 2)
|
||
else:
|
||
new_cash = round(current + amount, 2)
|
||
if new_cash < 0:
|
||
new_cash = 0
|
||
ok, _ = db_update_available_cash(user_id, new_cash)
|
||
if ok:
|
||
return jsonify({'success': True, 'trade': trade, 'available_cash': new_cash})
|
||
|
||
return jsonify({'success': True, 'trade': trade})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 400
|
||
|
||
|
||
@bp.route('/trades/<int:trade_id>', methods=['PUT'])
|
||
@login_required
|
||
def update_trade(trade_id):
|
||
"""更新交易记录"""
|
||
try:
|
||
user_id = get_current_user_id()
|
||
data = request.get_json()
|
||
|
||
# 处理数值(空字符串转为None)
|
||
def parse_float(val):
|
||
if val is None or val == '':
|
||
return None
|
||
try:
|
||
return round(float(val), 4)
|
||
except:
|
||
return None
|
||
|
||
def parse_int(val):
|
||
if val is None or val == '':
|
||
return None
|
||
try:
|
||
return int(val)
|
||
except:
|
||
return None
|
||
|
||
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'))
|
||
|
||
old_trade = db_get_trade(user_id, trade_id)
|
||
if not old_trade:
|
||
return jsonify({'error': '交易记录不存在'}), 404
|
||
|
||
trade, error = db_update_trade(user_id, trade_id, data)
|
||
if error:
|
||
return jsonify({'success': False, 'error': error}), 400
|
||
if not trade:
|
||
return jsonify({'error': '交易记录不存在'}), 404
|
||
|
||
# 根据修改同步调整可用资金:先回滚旧交易,再应用新交易
|
||
def trade_amount(t):
|
||
p, q = (t.get('price') or 0), (t.get('quantity') or 0)
|
||
return round(float(p) * int(q), 2) if p and q else 0
|
||
old_amt = trade_amount(old_trade)
|
||
new_amt = trade_amount(trade)
|
||
old_type = (old_trade.get('trade_type') or '').lower()
|
||
new_type = (trade.get('trade_type') or '').lower()
|
||
delta = 0
|
||
if old_type == 'buy':
|
||
delta += old_amt
|
||
elif old_type == 'sell':
|
||
delta -= old_amt
|
||
if new_type == 'buy':
|
||
delta -= new_amt
|
||
elif new_type == 'sell':
|
||
delta += new_amt
|
||
if delta != 0:
|
||
current = db_get_available_cash(user_id)
|
||
new_cash = max(0, round(current + delta, 2))
|
||
ok, _ = db_update_available_cash(user_id, new_cash)
|
||
if ok:
|
||
return jsonify({'success': True, 'trade': trade, 'available_cash': new_cash})
|
||
|
||
return jsonify({'success': True, 'trade': trade})
|
||
except Exception as e:
|
||
return jsonify({'error': str(e)}), 400
|
||
|
||
|
||
@bp.route('/trades/<int:trade_id>', methods=['DELETE'])
|
||
@login_required
|
||
def delete_trade(trade_id):
|
||
"""删除交易记录"""
|
||
user_id = get_current_user_id()
|
||
old_trade = db_get_trade(user_id, trade_id)
|
||
if not old_trade:
|
||
return jsonify({'success': False, 'error': '交易记录不存在'}), 404
|
||
success = db_delete_trade(user_id, trade_id)
|
||
if not success:
|
||
return jsonify({'success': False}), 400
|
||
# 回滚该交易对可用资金的影响
|
||
t_type = (old_trade.get('trade_type') or '').lower()
|
||
amount = round(float(old_trade.get('price') or 0) * int(old_trade.get('quantity') or 0), 2)
|
||
delta = amount if t_type == 'buy' else -amount
|
||
if delta != 0:
|
||
current = db_get_available_cash(user_id)
|
||
new_cash = max(0, round(current + delta, 2))
|
||
db_update_available_cash(user_id, new_cash)
|
||
return jsonify({'success': True, 'available_cash': new_cash})
|
||
return jsonify({'success': True})
|
||
|
||
|
||
@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
|