feat: 新增外部因素分析模块+综合评分引擎+算法文档重构

新增模块:
- fund_flow_analyzer.py: 主力资金流向分析(P0, ±20)
- market_sentiment.py: 市场情绪指标(P1, ±10)
- external_factors.py: 北向资金/美股/大宗商品/汇率(P2-P4,P7)
- news_analyzer.py: 公告/并购/政策面LLM分析(P5-P6)
- score_engine.py: 综合评分引擎,整合技术面+外部因素

路由更新:
- analysis.py: deep_analyze接入综合评分,根据最终评级修正买卖建议
- market.py: 新增4个外部因素API端点
- trades.py: 交易路由更新

算法文档重构:
- 章节重排: 技术面(二三)→外部因素(四)→买卖决策(五)→数据源(六)→性能(七)
- 架构图更新为五层,标注章节对应
- 5.1/5.2标注纯技术面,5.3整合外部因素修正推荐
This commit is contained in:
selfrelease
2026-07-17 23:50:42 +08:00
parent 04f8b9f951
commit 2b5a32ca1e
23 changed files with 4191 additions and 268 deletions
+171 -123
View File
@@ -1,16 +1,47 @@
"""
交易记录 API 路由(纯数据库版)
交易记录 API 路由(纯数据库版,事务安全
"""
from flask import Blueprint, request, jsonify, session
from psycopg2.extras import RealDictCursor
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
login_required, get_current_user_id, get_db, put_db,
db_get_trades, db_get_available_cash, db_update_available_cash
)
bp = Blueprint('trades', __name__, url_prefix='/api')
def _parse_float(val):
if val is None or val == '':
return None
try:
return round(float(val), 4)
except Exception:
return None
def _parse_int(val):
if val is None or val == '':
return None
try:
return int(val)
except Exception:
return None
def _calc_cash_delta(trade_type, price, quantity):
"""计算交易对可用资金的影响"""
if not trade_type or price is None or quantity is None:
return 0
amount = round(float(price) * int(quantity), 2)
t = trade_type.lower()
if t == 'buy':
return -amount
elif t == 'sell':
return amount
return 0
@bp.route('/trades', methods=['GET'])
@login_required
def get_trades():
@@ -23,153 +54,170 @@ def get_trades():
@bp.route('/trades', methods=['POST'])
@login_required
def add_trade():
"""添加交易记录"""
"""添加交易记录(trade 插入 + 可用资金更新在同一事务中)"""
conn = get_db()
if not conn:
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
try:
user_id = get_current_user_id()
data = request.get_json()
# 处理数值(空字符串转为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})
data['price'] = _parse_float(data.get('price'))
data['quantity'] = _parse_int(data.get('quantity'))
data['profit_amount'] = _parse_float(data.get('profit_amount'))
data['stop_loss_price'] = _parse_float(data.get('stop_loss_price'))
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
INSERT INTO trades (user_id, stock_code, stock_name, trade_type, price,
quantity, trade_date, reason, result, profit_amount,
stop_loss_price, notes)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price,
notes, created_at::text
""", (user_id, data.get('stock_code'), data.get('stock_name'),
data.get('trade_type'), data.get('price'), data.get('quantity'),
data.get('trade_date'), data.get('reason'), data.get('result'),
data.get('profit_amount'), data.get('stop_loss_price'), data.get('notes')))
trade = cur.fetchone()
delta = _calc_cash_delta(data.get('trade_type'), data.get('price'), data.get('quantity'))
new_cash = None
if delta != 0:
cur.execute("SELECT available_cash FROM users WHERE id = %s FOR UPDATE", (user_id,))
row = cur.fetchone()
current = float(row['available_cash'] or 0) if row else 0
new_cash = max(0, round(current + delta, 2))
cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (new_cash, user_id))
conn.commit()
resp = {'success': True, 'trade': trade}
if new_cash is not None:
resp['available_cash'] = new_cash
return jsonify(resp)
except Exception as e:
return jsonify({'error': str(e)}), 400
conn.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
finally:
put_db(conn)
@bp.route('/trades/<int:trade_id>', methods=['PUT'])
@login_required
def update_trade(trade_id):
"""更新交易记录"""
"""更新交易记录(同一事务内回滚旧资金 + 应用新资金)"""
conn = get_db()
if not conn:
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
try:
user_id = get_current_user_id()
data = request.get_json()
# 处理数值(空字符串转为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'))
data['price'] = _parse_float(data.get('price'))
if 'quantity' in data:
data['quantity'] = parse_int(data.get('quantity'))
data['quantity'] = _parse_int(data.get('quantity'))
if 'profit_amount' in data:
data['profit_amount'] = parse_float(data.get('profit_amount'))
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)
data['stop_loss_price'] = _parse_float(data.get('stop_loss_price'))
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price, notes
FROM trades WHERE id = %s AND user_id = %s
""", (trade_id, user_id))
old_trade = cur.fetchone()
if not old_trade:
put_db(conn)
return jsonify({'error': '交易记录不存在'}), 404
trade, error = db_update_trade(user_id, trade_id, data)
if error:
return jsonify({'success': False, 'error': error}), 400
cur.execute("""
UPDATE trades SET
stock_code = COALESCE(%s, stock_code), stock_name = COALESCE(%s, stock_name),
trade_type = COALESCE(%s, trade_type), price = COALESCE(%s, price),
quantity = COALESCE(%s, quantity), trade_date = COALESCE(%s, trade_date),
reason = COALESCE(%s, reason), result = COALESCE(%s, result),
profit_amount = COALESCE(%s, profit_amount),
stop_loss_price = COALESCE(%s, stop_loss_price), notes = COALESCE(%s, notes)
WHERE id = %s AND user_id = %s
RETURNING id, stock_code, stock_name, trade_type, price, quantity,
trade_date::text, reason, result, profit_amount, stop_loss_price,
notes, created_at::text
""", (data.get('stock_code'), data.get('stock_name'), data.get('trade_type'),
data.get('price'), data.get('quantity'), data.get('trade_date'),
data.get('reason'), data.get('result'), data.get('profit_amount'),
data.get('stop_loss_price'), data.get('notes'), trade_id, user_id))
trade = cur.fetchone()
if not trade:
conn.rollback()
put_db(conn)
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
old_delta = _calc_cash_delta(old_trade.get('trade_type'), old_trade.get('price'), old_trade.get('quantity'))
new_delta = _calc_cash_delta(trade.get('trade_type'), trade.get('price'), trade.get('quantity'))
delta = new_delta - old_delta
new_cash = None
if delta != 0:
current = db_get_available_cash(user_id)
cur.execute("SELECT available_cash FROM users WHERE id = %s FOR UPDATE", (user_id,))
row = cur.fetchone()
current = float(row['available_cash'] or 0) if row else 0
new_cash = max(0, round(current + delta, 2))
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})
cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (new_cash, user_id))
conn.commit()
resp = {'success': True, 'trade': trade}
if new_cash is not None:
resp['available_cash'] = new_cash
return jsonify(resp)
except Exception as e:
return jsonify({'error': str(e)}), 400
conn.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
finally:
put_db(conn)
@bp.route('/trades/<int:trade_id>', methods=['DELETE'])
@login_required
def delete_trade(trade_id):
"""删除交易记录"""
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})
"""删除交易记录(同一事务内删除 + 回滚资金)"""
conn = get_db()
if not conn:
return jsonify({'success': False, 'error': '数据库连接失败'}), 500
try:
user_id = get_current_user_id()
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT trade_type, price, quantity FROM trades WHERE id = %s AND user_id = %s
""", (trade_id, user_id))
old_trade = cur.fetchone()
if not old_trade:
put_db(conn)
return jsonify({'success': False, 'error': '交易记录不存在'}), 404
cur.execute("DELETE FROM trades WHERE id = %s AND user_id = %s", (trade_id, user_id))
delta = -_calc_cash_delta(old_trade.get('trade_type'), old_trade.get('price'), old_trade.get('quantity'))
new_cash = None
if delta != 0:
cur.execute("SELECT available_cash FROM users WHERE id = %s FOR UPDATE", (user_id,))
row = cur.fetchone()
current = float(row['available_cash'] or 0) if row else 0
new_cash = max(0, round(current + delta, 2))
cur.execute("UPDATE users SET available_cash = %s WHERE id = %s", (new_cash, user_id))
conn.commit()
resp = {'success': True}
if new_cash is not None:
resp['available_cash'] = new_cash
return jsonify(resp)
except Exception as e:
conn.rollback()
return jsonify({'success': False, 'error': str(e)}), 400
finally:
put_db(conn)
@bp.route('/available_cash', methods=['GET'])