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:
@@ -11,6 +11,7 @@ from services.stock_service import (
|
||||
from services.stock_algorithms import (
|
||||
compute_recommend, get_kline_data as algo_get_kline_data,
|
||||
compute_bull_stage, find_bull_stocks, BULL_STAGES,
|
||||
compute_deep_analysis,
|
||||
)
|
||||
from db import (
|
||||
login_required, get_current_user_id,
|
||||
@@ -88,6 +89,143 @@ def analyze():
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/deep_analyze', methods=['POST'])
|
||||
def deep_analyze():
|
||||
"""单股深度分析(价格位置、压力支撑、量价、空间、综合评分)"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
stock_code = data.get('stock_code', '').strip()
|
||||
if not stock_code:
|
||||
return jsonify({'error': '股票代码不能为空'}), 400
|
||||
|
||||
df = algo_get_kline_data(stock_code, days=180)
|
||||
if df is None or len(df) < 30:
|
||||
return jsonify({'error': 'K线数据不足'}), 400
|
||||
|
||||
from services.technical_indicators import calc_all_indicators
|
||||
from services.signal_detector import detect_all_signals
|
||||
df = calc_all_indicators(df)
|
||||
|
||||
signal_result = detect_all_signals(df, lookback=5)
|
||||
|
||||
from db import get_db, put_db
|
||||
from psycopg2.extras import RealDictCursor
|
||||
realtime_info = None
|
||||
conn = get_db()
|
||||
if conn:
|
||||
try:
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
cur.execute("""
|
||||
SELECT code, name, price, change_pct, volume, amount,
|
||||
high, low, open, prev_close, pe, pb, total_market_cap
|
||||
FROM stock_realtime_price WHERE code = %s
|
||||
""", (stock_code,))
|
||||
realtime_info = cur.fetchone()
|
||||
finally:
|
||||
put_db(conn)
|
||||
|
||||
report = compute_deep_analysis(df, signal_result, realtime_info)
|
||||
|
||||
stock_name = get_stock_name(stock_code) or (realtime_info or {}).get('name', '')
|
||||
|
||||
sig_status = signal_result.get('signal_status', [])
|
||||
indicators = signal_result.get('indicators', {})
|
||||
sig_count = signal_result.get('signal_summary', {}).get('total_signals', 0)
|
||||
rec = compute_recommend(sig_status, indicators, sig_count, False)
|
||||
|
||||
report['stock_code'] = stock_code
|
||||
report['stock_name'] = stock_name
|
||||
report['recommend'] = {
|
||||
'signal_type': rec[0],
|
||||
'display': rec[1],
|
||||
'reason': rec[2],
|
||||
'rate': rec[3],
|
||||
}
|
||||
report['signals'] = signal_result.get('signals', [])
|
||||
report['signal_status'] = sig_status
|
||||
|
||||
# ---- 综合评分引擎:整合外部因素(P0-P7)----
|
||||
try:
|
||||
from services.score_engine import compute_comprehensive_score
|
||||
tech_score = report.get('deep_score', 50)
|
||||
comprehensive = compute_comprehensive_score(
|
||||
stock_code, stock_name, tech_score, df
|
||||
)
|
||||
report['comprehensive'] = comprehensive
|
||||
# 用综合评分更新最终评分和评级
|
||||
report['deep_score'] = comprehensive['final_score']
|
||||
report['verdict'] = comprehensive['verdict']
|
||||
report['score_reasons'].extend(comprehensive.get('all_reasons', []))
|
||||
|
||||
# ---- 根据综合评级修正买卖建议 ----
|
||||
# 技术面推荐(compute_recommend)不含外部因素,
|
||||
# 当综合评级与技术面推荐矛盾时,以综合评级为准调整推荐
|
||||
final_score = comprehensive['final_score']
|
||||
final_verdict = comprehensive['verdict']
|
||||
orig_display = report['recommend'].get('display', '')
|
||||
orig_reason = report['recommend'].get('reason', '')
|
||||
orig_rate = report['recommend'].get('rate', 0)
|
||||
|
||||
# 综合评级偏空但技术面建议买入/加仓 → 降级为关注
|
||||
if final_score < 50 and orig_display in ('买入', '加仓'):
|
||||
report['recommend'] = {
|
||||
'signal_type': 'watch',
|
||||
'display': '关注',
|
||||
'reason': f"技术面信号偏多,但综合评级「{final_verdict}」(外部因素拖累),建议观望",
|
||||
'rate': final_score,
|
||||
}
|
||||
# 综合评级强烈看多但技术面建议观望/关注 → 升级为买入
|
||||
elif final_score >= 80 and orig_display in ('观望', '关注', '观察'):
|
||||
report['recommend'] = {
|
||||
'signal_type': 'buy',
|
||||
'display': '买入',
|
||||
'reason': f"技术面{orig_display},但综合评级「{final_verdict}」(外部因素共振看好),建议买入",
|
||||
'rate': final_score,
|
||||
}
|
||||
# 综合评级看空但技术面建议持有 → 降级为卖出
|
||||
elif final_score < 35 and orig_display in ('持有', '观望'):
|
||||
report['recommend'] = {
|
||||
'signal_type': 'sell',
|
||||
'display': '卖出',
|
||||
'reason': f"技术面{orig_display},但综合评级「{final_verdict}」(外部因素重大利空),建议卖出",
|
||||
'rate': final_score,
|
||||
}
|
||||
# 其他情况保持技术面推荐,但更新评分为综合评分
|
||||
else:
|
||||
report['recommend']['rate'] = final_score
|
||||
except Exception as e:
|
||||
print(f"综合评分引擎计算失败,使用技术面评分: {e}")
|
||||
|
||||
if realtime_info:
|
||||
report['realtime'] = {
|
||||
'price': float(realtime_info.get('price') or 0),
|
||||
'change_pct': float(realtime_info.get('change_pct') or 0),
|
||||
'pe': float(realtime_info.get('pe') or 0),
|
||||
'pb': float(realtime_info.get('pb') or 0),
|
||||
'total_market_cap': float(realtime_info.get('total_market_cap') or 0),
|
||||
'volume': int(realtime_info.get('volume') or 0),
|
||||
}
|
||||
|
||||
skip_llm = data.get('skip_llm', False)
|
||||
if report.get('ai_summary') and not skip_llm:
|
||||
try:
|
||||
polished = _llm_polish_summary(
|
||||
stock_name, stock_code, report['ai_summary'],
|
||||
report.get('deep_score', 0), report.get('verdict', '')
|
||||
)
|
||||
if polished:
|
||||
report['ai_summary']['text'] = polished['text']
|
||||
report['ai_summary']['action_tip'] = polished['action_tip']
|
||||
except Exception as e:
|
||||
print(f"LLM润色失败,使用规则文本: {e}")
|
||||
|
||||
return jsonify({'success': True, 'report': report})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/realtime_price/<stock_code>', methods=['GET'])
|
||||
def realtime_price(stock_code):
|
||||
"""获取实时价格(直接调用实时API,不使用数据库缓存)"""
|
||||
@@ -1178,3 +1316,73 @@ def get_bull_stocks():
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
def _llm_polish_summary(stock_name, stock_code, ai_summary, score, verdict):
|
||||
"""调用豆包LLM将规则模板生成的分析文本润色成更自然流畅的表达"""
|
||||
import requests as _req
|
||||
from config import Config
|
||||
|
||||
api_key = Config.DOUBAO_API_KEY
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
draft_text = ai_summary.get('text', '')
|
||||
draft_action = ai_summary.get('action_tip', '')
|
||||
|
||||
prompt = f"""你是一位资深股票分析师,擅长用通俗易懂的语言给普通投资者解读技术分析。
|
||||
|
||||
以下是对{stock_name}({stock_code})的技术分析草稿,综合评分{score}分({verdict}):
|
||||
|
||||
【分析草稿】
|
||||
{draft_text}
|
||||
|
||||
【操作建议草稿】
|
||||
{draft_action}
|
||||
|
||||
请你将上面的草稿改写成更自然、更生动的表达。要求:
|
||||
1. 用口语化表达,像老朋友聊天一样,避免专业术语堆砌
|
||||
2. 保留所有关键数据和结论,不要遗漏
|
||||
3. 适当加入比喻或生活化的表达,让小白也能听懂
|
||||
4. 操作建议要明确、具体,有可操作性
|
||||
5. 总字数控制在200字以内
|
||||
6. 不要用markdown格式,纯文本即可
|
||||
|
||||
请严格按以下JSON格式输出,不要输出其他内容:
|
||||
{{"text": "润色后的分析文本", "action_tip": "润色后的操作建议"}}"""
|
||||
|
||||
try:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}"
|
||||
}
|
||||
payload = {
|
||||
"model": "doubao-seed-1-6-251015",
|
||||
"max_completion_tokens": 2048,
|
||||
"stream": False,
|
||||
"messages": [{"role": "user", "content": prompt}]
|
||||
}
|
||||
resp = _req.post(
|
||||
"https://ark.cn-beijing.volces.com/api/v3/chat/completions",
|
||||
headers=headers, json=payload, timeout=45
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
|
||||
data = resp.json()
|
||||
content = data.get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||
if not content:
|
||||
return None
|
||||
|
||||
content = content.strip()
|
||||
if content.startswith('```'):
|
||||
content = content.split('\n', 1)[-1].rsplit('```', 1)[0].strip()
|
||||
|
||||
import json as _json
|
||||
result = _json.loads(content)
|
||||
if result.get('text') and result.get('action_tip'):
|
||||
return result
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"LLM polish error: {e}")
|
||||
return None
|
||||
|
||||
@@ -113,15 +113,17 @@ def get_current_user():
|
||||
email = session.get('email', session.get('username', ''))
|
||||
is_admin = False
|
||||
try:
|
||||
from db import get_db
|
||||
from db import get_db, put_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()
|
||||
try:
|
||||
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])
|
||||
finally:
|
||||
put_db(conn)
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({
|
||||
|
||||
@@ -523,3 +523,52 @@ def get_fundamental(stock_code):
|
||||
return jsonify({'success': True, 'data': result, 'source': 'api'})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============ 市场情绪 & 外部因素 API ============
|
||||
|
||||
@bp.route('/market_sentiment', methods=['GET'])
|
||||
def market_sentiment():
|
||||
"""获取市场情绪指标(涨停跌停比、连板高度、换手率中位数、两市成交额)"""
|
||||
try:
|
||||
from services.market_sentiment import calc_market_sentiment
|
||||
result = calc_market_sentiment()
|
||||
return jsonify({'success': True, 'data': result})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/external_factors', methods=['GET'])
|
||||
def external_factors():
|
||||
"""获取外部因素综合数据(北向资金、美股隔夜、大宗商品、汇率)"""
|
||||
try:
|
||||
from services.external_factors import get_all_external_factors
|
||||
result = get_all_external_factors()
|
||||
return jsonify({'success': True, 'data': result})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/fund_flow_analysis/<stock_code>', methods=['GET'])
|
||||
def fund_flow_analysis(stock_code):
|
||||
"""获取个股资金流向分析(P0:主力资金进出评分和信号)"""
|
||||
try:
|
||||
from services.fund_flow_analyzer import analyze_fund_flow
|
||||
days = request.args.get('days', 5, type=int)
|
||||
result = analyze_fund_flow(stock_code, days=days)
|
||||
return jsonify({'success': True, 'data': result})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@bp.route('/news_analysis/<stock_code>', methods=['GET'])
|
||||
def news_analysis(stock_code):
|
||||
"""获取个股消息面分析(P5公告+P6政策+异动检测)"""
|
||||
try:
|
||||
from services.news_analyzer import analyze_news_factors
|
||||
from services.stock_service import get_stock_name
|
||||
stock_name = get_stock_name(stock_code) or ''
|
||||
result = analyze_news_factors(stock_code, stock_name)
|
||||
return jsonify({'success': True, 'data': result})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
+171
-123
@@ -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'])
|
||||
|
||||
Reference in New Issue
Block a user