Initial commit

This commit is contained in:
freedakgmail
2026-07-17 18:49:07 +08:00
commit 9c7d7abdd4
100 changed files with 41337 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# Routes 模块
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+136
View File
@@ -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})
+525
View File
@@ -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
+733
View File
@@ -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()
+287
View File
@@ -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()
+313
View File
@@ -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
+47
View File
@@ -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 []})