Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Routes 模块
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
用户认证 API 路由
|
||||
使用邮箱和密码进行登录/注册
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify, session
|
||||
from db import create_user, verify_user
|
||||
|
||||
bp = Blueprint('auth', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@bp.route('/register', methods=['POST'])
|
||||
def register():
|
||||
"""用户注册"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({'success': False, 'error': '邮箱和密码不能为空'}), 400
|
||||
|
||||
# 验证邮箱格式
|
||||
import re
|
||||
if not re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', email):
|
||||
return jsonify({'success': False, 'error': '请输入有效的邮箱地址'}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({'success': False, 'error': '密码至少6位'}), 400
|
||||
|
||||
user, error = create_user(email, password)
|
||||
if error:
|
||||
return jsonify({'success': False, 'error': error}), 400
|
||||
|
||||
# 自动登录
|
||||
session['user_id'] = user['id']
|
||||
session['username'] = user['username'] # username存的是email
|
||||
session['email'] = user['username']
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'user': {'id': user['id'], 'email': user['username'], 'username': user['username'].split('@')[0]}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/login', methods=['POST'])
|
||||
def login():
|
||||
"""用户登录"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
email = data.get('email', '').strip().lower()
|
||||
password = data.get('password', '')
|
||||
|
||||
if not email or not password:
|
||||
return jsonify({'success': False, 'error': '邮箱和密码不能为空'}), 400
|
||||
|
||||
user, error = verify_user(email, password)
|
||||
if error:
|
||||
return jsonify({'success': False, 'error': error}), 400
|
||||
|
||||
session['user_id'] = user['id']
|
||||
session['username'] = user['username']
|
||||
session['email'] = user['username']
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'user': {'id': user['id'], 'email': user['username'], 'username': user['username'].split('@')[0]}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/logout', methods=['POST'])
|
||||
def logout():
|
||||
"""用户登出"""
|
||||
session.clear()
|
||||
return jsonify({'success': True})
|
||||
|
||||
|
||||
@bp.route('/change_password', methods=['POST'])
|
||||
def change_password():
|
||||
"""修改密码"""
|
||||
if 'user_id' not in session:
|
||||
return jsonify({'success': False, 'error': '请先登录'}), 401
|
||||
|
||||
try:
|
||||
data = request.get_json()
|
||||
old_password = data.get('old_password', '')
|
||||
new_password = data.get('new_password', '')
|
||||
|
||||
if not old_password or not new_password:
|
||||
return jsonify({'success': False, 'error': '请填写所有字段'}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({'success': False, 'error': '新密码至少6位'}), 400
|
||||
|
||||
from db import change_user_password
|
||||
success, error = change_user_password(session['user_id'], old_password, new_password)
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True})
|
||||
else:
|
||||
return jsonify({'success': False, 'error': error}), 400
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/me', methods=['GET'])
|
||||
def get_current_user():
|
||||
"""获取当前用户"""
|
||||
if 'user_id' in session:
|
||||
email = session.get('email', session.get('username', ''))
|
||||
is_admin = False
|
||||
try:
|
||||
from db import get_db
|
||||
conn = get_db()
|
||||
if conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT is_admin FROM users WHERE id = %s", (session['user_id'],))
|
||||
row = cur.fetchone()
|
||||
if row:
|
||||
is_admin = bool(row[0])
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'user': {
|
||||
'id': session['user_id'],
|
||||
'email': email,
|
||||
'username': email.split('@')[0] if '@' in email else email,
|
||||
'is_admin': is_admin
|
||||
}
|
||||
})
|
||||
return jsonify({'success': False, 'user': None})
|
||||
@@ -0,0 +1,525 @@
|
||||
"""
|
||||
市场数据 API 路由
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
from services.stock_service import get_stock_fund_flow, load_cached_data
|
||||
from services.stock_algorithms import get_kline_data as algo_get_kline_data
|
||||
from db import get_db
|
||||
|
||||
bp = Blueprint('market', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
# ============ 数据库查询API(高速版) ============
|
||||
|
||||
@bp.route('/db/realtime_price/<stock_code>', methods=['GET'])
|
||||
def db_realtime_price(stock_code):
|
||||
"""从数据库获取实时价格(毫秒级响应)"""
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||
|
||||
try:
|
||||
from psycopg2.extras import RealDictCursor
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
cur.execute("""
|
||||
SELECT code, name, price, change_pct, change_amount,
|
||||
volume, amount, high, low, open, prev_close,
|
||||
pe, pb, total_market_cap, updated_at::text
|
||||
FROM stock_realtime_price
|
||||
WHERE code = %s
|
||||
""", (stock_code,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({'success': False, 'error': '未找到数据'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': dict(row)
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@bp.route('/db/realtime_prices', methods=['POST'])
|
||||
def db_realtime_prices():
|
||||
"""批量获取实时价格"""
|
||||
data = request.get_json()
|
||||
codes = data.get('codes', [])
|
||||
|
||||
if not codes:
|
||||
return jsonify({'success': True, 'data': []})
|
||||
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||
|
||||
try:
|
||||
from psycopg2.extras import RealDictCursor
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
cur.execute("""
|
||||
SELECT code, name, price, change_pct, pe, pb, total_market_cap, updated_at::text
|
||||
FROM stock_realtime_price
|
||||
WHERE code = ANY(%s)
|
||||
""", (codes,))
|
||||
rows = cur.fetchall()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': [dict(row) for row in rows]
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@bp.route('/db/fund_flow_today/<stock_code>', methods=['GET'])
|
||||
def db_fund_flow_today(stock_code):
|
||||
"""从数据库获取今日资金流向"""
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||
|
||||
try:
|
||||
from psycopg2.extras import RealDictCursor
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
cur.execute("""
|
||||
SELECT code, name, main_net_inflow, main_net_inflow_pct,
|
||||
super_net_inflow, super_net_inflow_pct,
|
||||
big_net_inflow, big_net_inflow_pct,
|
||||
price, change_pct, updated_at::text
|
||||
FROM stock_fund_flow_today
|
||||
WHERE code = %s
|
||||
""", (stock_code,))
|
||||
row = cur.fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({'success': False, 'error': '未找到数据'}), 404
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': dict(row)
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@bp.route('/db/fund_flow_today_batch', methods=['POST'])
|
||||
def db_fund_flow_today_batch():
|
||||
"""批量获取今日资金流向"""
|
||||
data = request.get_json()
|
||||
codes = data.get('codes', [])
|
||||
|
||||
if not codes:
|
||||
return jsonify({'success': True, 'data': []})
|
||||
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||
|
||||
try:
|
||||
from psycopg2.extras import RealDictCursor
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
cur.execute("""
|
||||
SELECT code, name, main_net_inflow, main_net_inflow_pct,
|
||||
super_net_inflow, super_net_inflow_pct,
|
||||
price, change_pct, updated_at::text
|
||||
FROM stock_fund_flow_today
|
||||
WHERE code = ANY(%s)
|
||||
""", (codes,))
|
||||
rows = cur.fetchall()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': [dict(row) for row in rows]
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
@bp.route('/db/data_status', methods=['GET'])
|
||||
def db_data_status():
|
||||
"""获取数据更新状态"""
|
||||
conn = get_db()
|
||||
if not conn:
|
||||
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
|
||||
|
||||
try:
|
||||
from psycopg2.extras import RealDictCursor
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 获取各表数据统计
|
||||
cur.execute("SELECT COUNT(*) as count, MAX(updated_at)::text as last_update FROM stock_realtime_price")
|
||||
price_stats = cur.fetchone()
|
||||
|
||||
cur.execute("SELECT COUNT(*) as count, MAX(updated_at)::text as last_update FROM stock_fund_flow_today")
|
||||
flow_stats = cur.fetchone()
|
||||
|
||||
cur.execute("""
|
||||
SELECT data_type, status, records_count, finished_at::text
|
||||
FROM data_update_log
|
||||
ORDER BY finished_at DESC
|
||||
LIMIT 5
|
||||
""")
|
||||
logs = cur.fetchall()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'realtime_price': dict(price_stats) if price_stats else {},
|
||||
'fund_flow_today': dict(flow_stats) if flow_stats else {},
|
||||
'recent_logs': [dict(log) for log in logs]
|
||||
})
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ============ 原有API(兼容) ============
|
||||
|
||||
|
||||
@bp.route('/hot_stocks', methods=['GET'])
|
||||
def hot_stocks():
|
||||
"""人气榜 — 已删除(东方财富API不可用,无替代源)"""
|
||||
return jsonify({'success': False, 'error': '人气榜功能已停用', 'data': [], 'total': 0}), 410
|
||||
|
||||
|
||||
@bp.route('/kline/<stock_code>', methods=['GET'])
|
||||
def get_kline(stock_code):
|
||||
"""获取K线数据 — 使用统一算法模块 services.stock_algorithms"""
|
||||
try:
|
||||
period = request.args.get('period', 'daily')
|
||||
days_map = {
|
||||
'weekly': 7,
|
||||
'monthly': 30,
|
||||
'quarterly': 90,
|
||||
'yearly': 365
|
||||
}
|
||||
days = days_map.get(period, 30)
|
||||
|
||||
# 使用统一K线获取(含5种数据源自动回退)
|
||||
df = algo_get_kline_data(stock_code, days=days, use_local_db=True)
|
||||
if df is None or df.empty:
|
||||
return jsonify({'success': True, 'data': [], 'stock_code': stock_code, 'period': period})
|
||||
|
||||
kline_data = []
|
||||
for _, row in df.iterrows():
|
||||
d = row.get('date', '')
|
||||
kline_data.append({
|
||||
'date': d.strftime('%Y-%m-%d') if hasattr(d, 'strftime') else str(d),
|
||||
'open': float(row.get('open', 0)),
|
||||
'close': float(row.get('close', 0)),
|
||||
'high': float(row.get('high', 0)),
|
||||
'low': float(row.get('low', 0)),
|
||||
'volume': float(row.get('volume', 0)),
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': kline_data,
|
||||
'stock_code': stock_code,
|
||||
'period': period
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"K线接口异常({stock_code}): {e}")
|
||||
return jsonify({'success': True, 'data': [], 'stock_code': stock_code, 'period': period})
|
||||
|
||||
|
||||
@bp.route('/fundflow/<stock_code>', methods=['GET'])
|
||||
def get_fundflow(stock_code):
|
||||
"""获取近N天资金流向(失败时返回空数据)"""
|
||||
try:
|
||||
days = request.args.get('days', 3, type=int)
|
||||
|
||||
try:
|
||||
cached_df, stock_name, _ = load_cached_data(stock_code)
|
||||
except Exception as e:
|
||||
print(f"加载缓存数据失败({stock_code}): {e}")
|
||||
cached_df, stock_name = None, None
|
||||
|
||||
if cached_df is None or cached_df.empty:
|
||||
try:
|
||||
end_date = datetime.now().strftime('%Y-%m-%d')
|
||||
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
||||
cached_df, stock_name, error = get_stock_fund_flow(stock_code, start_date, end_date)
|
||||
except Exception as e:
|
||||
print(f"获取资金流向失败({stock_code}): {e}")
|
||||
return jsonify({'success': True, 'stock_code': stock_code, 'stock_name': '', 'data': []})
|
||||
|
||||
if cached_df is None or cached_df.empty:
|
||||
return jsonify({'success': True, 'stock_code': stock_code, 'stock_name': stock_name or '', 'data': []})
|
||||
|
||||
cached_df = cached_df.sort_values('日期', ascending=False)
|
||||
recent = cached_df.head(days)
|
||||
|
||||
flow_data = []
|
||||
for _, row in recent.iterrows():
|
||||
flow_data.append({
|
||||
'date': row['日期'].strftime('%Y-%m-%d') if hasattr(row['日期'], 'strftime') else str(row['日期']),
|
||||
'price': float(row['收盘价']) if pd.notna(row['收盘价']) else 0,
|
||||
'change': float(row['涨跌幅']) if pd.notna(row['涨跌幅']) else 0,
|
||||
'super_ratio': float(row['超大单净流入-净占比']) if pd.notna(row['超大单净流入-净占比']) else 0,
|
||||
'main_ratio': float(row['主力净流入-净占比']) if pd.notna(row['主力净流入-净占比']) else 0,
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'stock_code': stock_code,
|
||||
'stock_name': stock_name or '',
|
||||
'data': flow_data
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"资金流向接口异常({stock_code}): {e}")
|
||||
return jsonify({'success': True, 'stock_code': stock_code, 'stock_name': '', 'data': []})
|
||||
|
||||
|
||||
@bp.route('/lhb', methods=['GET'])
|
||||
def get_lhb():
|
||||
"""龙虎榜 — 已删除(东方财富API不可用,无替代源)"""
|
||||
return jsonify({'success': False, 'error': '龙虎榜功能已停用', 'data': [], 'total': 0}), 410
|
||||
|
||||
|
||||
@bp.route('/fund_flow_rank', methods=['GET'])
|
||||
def get_fund_flow_rank():
|
||||
"""获取资金流向排行 — 从数据库缓存获取"""
|
||||
try:
|
||||
limit = request.args.get('limit', 50, type=int)
|
||||
|
||||
# 东方财富API已不可用,从数据库获取缓存数据
|
||||
from db import get_db as _get_db
|
||||
_conn = _get_db()
|
||||
if _conn:
|
||||
try:
|
||||
_cur = _conn.cursor()
|
||||
_cur.execute("""
|
||||
SELECT code, name, main_net_inflow, main_net_inflow_pct,
|
||||
price, change_pct
|
||||
FROM stock_fund_flow_today
|
||||
ORDER BY main_net_inflow DESC LIMIT %s
|
||||
""", (limit,))
|
||||
rows = _cur.fetchall()
|
||||
flow_data = [{'code': r[0], 'name': r[1],
|
||||
'main_net_inflow': float(r[2] or 0),
|
||||
'main_pct': float(r[3] or 0),
|
||||
'price': float(r[4] or 0),
|
||||
'change_pct': float(r[5] or 0)} for r in rows]
|
||||
return jsonify({'success': True, 'data': flow_data, 'total': len(flow_data),
|
||||
'source': 'cache'})
|
||||
finally:
|
||||
_conn.close()
|
||||
|
||||
return jsonify({'success': True, 'data': [], 'total': 0})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/fundamental/<stock_code>', methods=['GET'])
|
||||
def get_fundamental(stock_code):
|
||||
"""获取基本面数据(当日缓存版)+ 近三日资金流向 + 财务指标"""
|
||||
try:
|
||||
from db import db_get_fundamental, db_save_fundamental, get_db
|
||||
from services.stock_service import get_stock_name
|
||||
|
||||
# 获取近三日资金流向数据 + 技术信号
|
||||
fund_flow_3days = []
|
||||
realtime_data = None
|
||||
signal_data = None
|
||||
|
||||
try:
|
||||
conn = get_db()
|
||||
if conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("""
|
||||
SELECT trade_date, close_price, change_pct,
|
||||
main_net_inflow_pct, super_net_inflow_pct, big_net_inflow_pct
|
||||
FROM stock_fund_flow_history
|
||||
WHERE code = %s
|
||||
ORDER BY trade_date DESC
|
||||
LIMIT 3
|
||||
""", (stock_code,))
|
||||
rows = cur.fetchall()
|
||||
for row in rows:
|
||||
fund_flow_3days.append({
|
||||
'date': row[0].strftime('%m-%d') if row[0] else '',
|
||||
'close_price': float(row[1]) if row[1] else 0,
|
||||
'change_pct': float(row[2]) if row[2] else 0,
|
||||
'main_pct': float(row[3]) if row[3] else 0,
|
||||
'super_pct': float(row[4]) if row[4] else 0,
|
||||
'big_pct': float(row[5]) if row[5] else 0,
|
||||
})
|
||||
|
||||
cur.execute("""
|
||||
SELECT name, price, pe, pb, change_pct, total_market_cap
|
||||
FROM stock_realtime_price
|
||||
WHERE code = %s
|
||||
""", (stock_code,))
|
||||
rt_row = cur.fetchone()
|
||||
if rt_row:
|
||||
realtime_data = {
|
||||
'name': rt_row[0],
|
||||
'price': float(rt_row[1]) if rt_row[1] else None,
|
||||
'pe': float(rt_row[2]) if rt_row[2] else None,
|
||||
'pb': float(rt_row[3]) if rt_row[3] else None,
|
||||
'change_pct': float(rt_row[4]) if rt_row[4] else None,
|
||||
'total_market_cap': float(rt_row[5]) if rt_row[5] else None,
|
||||
}
|
||||
|
||||
from datetime import date as date_cls
|
||||
# 优先今天的扫描数据,无则回退到最近可用日期
|
||||
scan_date = date_cls.today().strftime('%Y-%m-%d')
|
||||
cur.execute("""
|
||||
SELECT signal_status, indicators, triggered_count
|
||||
FROM stock_signal_scan
|
||||
WHERE code = %s AND scan_date = %s
|
||||
""", (stock_code, scan_date))
|
||||
sig_row = cur.fetchone()
|
||||
if not sig_row:
|
||||
cur.execute("""
|
||||
SELECT signal_status, indicators, triggered_count
|
||||
FROM stock_signal_scan
|
||||
WHERE code = %s AND scan_date = (SELECT MAX(scan_date) FROM stock_signal_scan)
|
||||
""", (stock_code,))
|
||||
sig_row = cur.fetchone()
|
||||
if sig_row:
|
||||
import json as json_mod
|
||||
ss = sig_row[0] if isinstance(sig_row[0], list) else (json_mod.loads(sig_row[0]) if sig_row[0] else [])
|
||||
ind = sig_row[1] if isinstance(sig_row[1], dict) else (json_mod.loads(sig_row[1]) if sig_row[1] else {})
|
||||
signal_data = {
|
||||
'signal_status': ss,
|
||||
'indicators': ind,
|
||||
'triggered_count': sig_row[2] or 0,
|
||||
}
|
||||
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
print(f"获取数据失败: {e}")
|
||||
|
||||
# 优先从数据库获取当日缓存
|
||||
cached = db_get_fundamental(stock_code)
|
||||
if cached:
|
||||
# 优先使用实时价格表中的PE/PB数据
|
||||
pe_val = realtime_data['pe'] if realtime_data and realtime_data['pe'] else (float(cached['pe']) if cached['pe'] else '')
|
||||
pb_val = realtime_data['pb'] if realtime_data and realtime_data['pb'] else (float(cached['pb']) if cached['pb'] else '')
|
||||
price_val = realtime_data['price'] if realtime_data and realtime_data['price'] else (float(cached['latest_price']) if cached['latest_price'] else '')
|
||||
change_val = realtime_data['change_pct'] if realtime_data and realtime_data['change_pct'] else (float(cached['change_pct']) if cached['change_pct'] else '')
|
||||
market_cap_val = realtime_data['total_market_cap'] if realtime_data and realtime_data['total_market_cap'] else (float(cached['total_market_cap']) if cached['total_market_cap'] else '')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'stock_code': cached['code'],
|
||||
'stock_name': cached['name'],
|
||||
'pe_ttm': pe_val,
|
||||
'pb': pb_val,
|
||||
'total_market_cap': market_cap_val,
|
||||
'industry': cached['industry'] or '',
|
||||
'latest_price': price_val,
|
||||
'change_pct': change_val,
|
||||
'roe': float(cached['roe']) if cached.get('roe') else '',
|
||||
'eps': float(cached['eps']) if cached.get('eps') else '',
|
||||
'bps': float(cached['bps']) if cached.get('bps') else '',
|
||||
'revenue_yoy': float(cached['revenue_yoy']) if cached.get('revenue_yoy') else '',
|
||||
'profit_yoy': float(cached['profit_yoy']) if cached.get('profit_yoy') else '',
|
||||
'gross_margin': float(cached['gross_margin']) if cached.get('gross_margin') else '',
|
||||
'net_margin': float(cached['net_margin']) if cached.get('net_margin') else '',
|
||||
'fund_flow_3days': fund_flow_3days,
|
||||
'signal_data': signal_data,
|
||||
},
|
||||
'source': 'database'
|
||||
})
|
||||
|
||||
# 数据库没有基本面缓存,从实时价格表和API获取
|
||||
result = {
|
||||
'stock_code': stock_code,
|
||||
'stock_name': realtime_data['name'] if realtime_data else (get_stock_name(stock_code) or ''),
|
||||
'pe_ttm': realtime_data['pe'] if realtime_data and realtime_data['pe'] else '',
|
||||
'pb': realtime_data['pb'] if realtime_data and realtime_data['pb'] else '',
|
||||
'total_market_cap': realtime_data['total_market_cap'] if realtime_data and realtime_data['total_market_cap'] else '',
|
||||
'industry': '',
|
||||
'latest_price': realtime_data['price'] if realtime_data and realtime_data['price'] else '',
|
||||
'change_pct': realtime_data['change_pct'] if realtime_data and realtime_data['change_pct'] else '',
|
||||
'roe': '',
|
||||
'eps': '',
|
||||
'bps': '',
|
||||
'revenue_yoy': '',
|
||||
'profit_yoy': '',
|
||||
'gross_margin': '',
|
||||
'net_margin': '',
|
||||
}
|
||||
|
||||
# 优先使用mairuiapi获取数据
|
||||
try:
|
||||
from services.mairui_api import get_realtime_price as mairui_realtime, get_financial_indicators, get_company_info
|
||||
|
||||
# 获取实时价格
|
||||
rt_result = mairui_realtime(stock_code)
|
||||
if rt_result['success']:
|
||||
rt_data = rt_result['data']
|
||||
result['latest_price'] = rt_data.get('price', '')
|
||||
result['change_pct'] = rt_data.get('change', '')
|
||||
result['pe_ttm'] = rt_data.get('pe') or result['pe_ttm']
|
||||
result['pb'] = rt_data.get('pb') or result['pb']
|
||||
result['total_market_cap'] = rt_data.get('total_market_cap') or result['total_market_cap']
|
||||
|
||||
# 获取公司信息
|
||||
company_result = get_company_info(stock_code)
|
||||
if company_result['success']:
|
||||
company_data = company_result['data']
|
||||
result['stock_name'] = company_data.get('name') or result['stock_name']
|
||||
result['industry'] = company_data.get('industry') or result['industry']
|
||||
|
||||
# 获取财务指标
|
||||
fin_result = get_financial_indicators(stock_code)
|
||||
if fin_result['success']:
|
||||
fin_data = fin_result['data']
|
||||
result['eps'] = fin_data.get('eps') or ''
|
||||
result['bps'] = fin_data.get('bps') or ''
|
||||
result['roe'] = fin_data.get('roe') or ''
|
||||
result['gross_margin'] = fin_data.get('gross_margin') or ''
|
||||
result['net_margin'] = fin_data.get('net_margin') or ''
|
||||
result['revenue_yoy'] = fin_data.get('revenue_yoy') or ''
|
||||
result['profit_yoy'] = fin_data.get('profit_yoy') or ''
|
||||
except Exception as e:
|
||||
print(f"mairuiapi获取基本面失败: {e}")
|
||||
|
||||
# 备用方案:先试腾讯API,再试akshare
|
||||
try:
|
||||
import requests as _rq
|
||||
_tc = ('sh' if stock_code.startswith('6') else 'sz') + stock_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) > 46:
|
||||
result['stock_name'] = _ff[1] or result['stock_name']
|
||||
result['latest_price'] = _ff[3]
|
||||
result['total_market_cap'] = f'{float(_ff[45])*100000000:.0f}' if _ff[45].strip() else ''
|
||||
except Exception as e2:
|
||||
print(f"腾讯财经备用方案也失败: {e2}")
|
||||
|
||||
# 保存到数据库缓存
|
||||
try:
|
||||
db_save_fundamental(stock_code, {
|
||||
'name': result['stock_name'],
|
||||
'pe': float(result['pe_ttm']) if result['pe_ttm'] else None,
|
||||
'pb': float(result['pb']) if result['pb'] else None,
|
||||
'total_market_cap': float(result['total_market_cap']) if result['total_market_cap'] else None,
|
||||
'industry': result['industry'],
|
||||
'latest_price': float(result['latest_price']) if result['latest_price'] else None,
|
||||
'change_pct': float(str(result['change_pct']).replace('%', '')) if result['change_pct'] else None,
|
||||
'roe': float(result['roe']) if result['roe'] else None,
|
||||
'eps': float(result['eps']) if result['eps'] else None,
|
||||
'bps': float(result['bps']) if result['bps'] else None,
|
||||
'revenue_yoy': float(result['revenue_yoy']) if result['revenue_yoy'] else None,
|
||||
'profit_yoy': float(result['profit_yoy']) if result['profit_yoy'] else None,
|
||||
'gross_margin': float(result['gross_margin']) if result['gross_margin'] else None,
|
||||
'net_margin': float(result['net_margin']) if result['net_margin'] else None,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"保存基本面缓存失败: {e}")
|
||||
|
||||
result['fund_flow_3days'] = fund_flow_3days
|
||||
result['signal_data'] = signal_data
|
||||
|
||||
return jsonify({'success': True, 'data': result, 'source': 'api'})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -0,0 +1,733 @@
|
||||
"""
|
||||
模拟交易 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()
|
||||
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
智能交易引擎 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()
|
||||
@@ -0,0 +1,313 @@
|
||||
"""
|
||||
交易记录 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
|
||||
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
关注列表 API 路由(纯数据库版)
|
||||
"""
|
||||
from flask import Blueprint, request, jsonify
|
||||
from db import (
|
||||
login_required, get_current_user_id,
|
||||
db_get_watchlist, db_add_to_watchlist, db_remove_from_watchlist
|
||||
)
|
||||
|
||||
bp = Blueprint('watchlist', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@bp.route('/watchlist', methods=['GET'])
|
||||
@login_required
|
||||
def get_watchlist():
|
||||
"""获取关注列表"""
|
||||
user_id = get_current_user_id()
|
||||
watchlist = db_get_watchlist(user_id)
|
||||
return jsonify({'success': True, 'watchlist': watchlist})
|
||||
|
||||
|
||||
@bp.route('/watchlist', methods=['POST'])
|
||||
@login_required
|
||||
def add_to_watchlist():
|
||||
"""添加到关注列表"""
|
||||
try:
|
||||
user_id = get_current_user_id()
|
||||
data = request.get_json()
|
||||
code = data.get('code')
|
||||
name = data.get('name', f'股票{code}')
|
||||
|
||||
watchlist, error = db_add_to_watchlist(user_id, code, name)
|
||||
if error:
|
||||
return jsonify({'success': False, 'error': error}), 400
|
||||
|
||||
return jsonify({'success': True, 'watchlist': watchlist})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 400
|
||||
|
||||
|
||||
@bp.route('/watchlist/<stock_code>', methods=['DELETE'])
|
||||
@login_required
|
||||
def remove_from_watchlist(stock_code):
|
||||
"""从关注列表移除"""
|
||||
user_id = get_current_user_id()
|
||||
watchlist = db_remove_from_watchlist(user_id, stock_code)
|
||||
return jsonify({'success': True, 'watchlist': watchlist or []})
|
||||
Reference in New Issue
Block a user