Files
stock/stock-html/routes/sim_trade.py
T
freedakgmail 9c7d7abdd4 Initial commit
2026-07-17 18:49:35 +08:00

734 lines
28 KiB
Python

"""
模拟交易 API 路由
每个交易日10点根据推荐率最高的买入卖出方案进行自动操作
"""
from flask import Blueprint, request, jsonify
from datetime import datetime, date, time
from db import get_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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@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:
conn.close()
@bp.route('/trigger_trade', methods=['POST'])
@login_required
def trigger_trade():
"""手动触发交易 — 优先使用智能引擎,降级到旧引擎"""
user_id = get_current_user_id()
try:
# 优先使用智能引擎
try:
from services.smart_trade_engine import execute_smart_trade
conn = get_db()
if conn:
result = execute_smart_trade(conn, user_id, scan_date=None)
conn.close()
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}")
# 降级: 使用旧引擎
from services.scheduler import execute_auto_trade_for_user
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
conn.close()
else:
trade_quantity = 1000
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:
conn.close()