a
This commit is contained in:
parent
9490207537
commit
4f21240116
|
|
@ -5,6 +5,7 @@ from fastapi import APIRouter, Query, HTTPException, Header, Depends
|
|||
from typing import Optional
|
||||
from datetime import datetime, timedelta
|
||||
from collections import Counter
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
|
@ -146,6 +147,13 @@ async def get_trades(
|
|||
else:
|
||||
formatted_trade['exit_reason_display'] = ''
|
||||
|
||||
# 入场思路 entry_context 可能从 DB 以 JSON 字符串返回,解析为对象便于前端/分析使用
|
||||
if formatted_trade.get('entry_context') is not None and isinstance(formatted_trade['entry_context'], str):
|
||||
try:
|
||||
formatted_trade['entry_context'] = json.loads(formatted_trade['entry_context'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
formatted_trades.append(formatted_trade)
|
||||
|
||||
result = {
|
||||
|
|
|
|||
|
|
@ -393,6 +393,7 @@ class Trade:
|
|||
notional_usdt=None,
|
||||
margin_usdt=None,
|
||||
account_id: int = None,
|
||||
entry_context=None,
|
||||
):
|
||||
"""创建交易记录(使用北京时间)
|
||||
|
||||
|
|
@ -402,7 +403,7 @@ class Trade:
|
|||
quantity: 数量
|
||||
entry_price: 入场价
|
||||
leverage: 杠杆
|
||||
entry_reason: 入场原因
|
||||
entry_reason: 入场原因(简短文本)
|
||||
entry_order_id: 币安开仓订单号(可选,用于对账)
|
||||
stop_loss_price: 实际使用的止损价格(考虑了ATR等动态计算)
|
||||
take_profit_price: 实际使用的止盈价格(考虑了ATR等动态计算)
|
||||
|
|
@ -411,6 +412,7 @@ class Trade:
|
|||
atr: 开仓时使用的ATR值(可选)
|
||||
notional_usdt: 名义下单量(USDT,可选)
|
||||
margin_usdt: 保证金(USDT,可选)
|
||||
entry_context: 入场思路/过程(dict,将存为 JSON):信号强度、市场状态、趋势、过滤通过情况等,便于事后分析策略执行效果
|
||||
"""
|
||||
entry_time = get_beijing_time()
|
||||
|
||||
|
|
@ -474,6 +476,15 @@ class Trade:
|
|||
columns.append("take_profit_2")
|
||||
values.append(take_profit_2)
|
||||
|
||||
if _has_column("entry_context") and entry_context is not None:
|
||||
try:
|
||||
entry_context_str = json.dumps(entry_context, ensure_ascii=False) if isinstance(entry_context, dict) else str(entry_context)
|
||||
except Exception:
|
||||
entry_context_str = None
|
||||
if entry_context_str is not None:
|
||||
columns.append("entry_context")
|
||||
values.append(entry_context_str)
|
||||
|
||||
placeholders = ", ".join(["%s"] * len(columns))
|
||||
sql = f"INSERT INTO trades ({', '.join(columns)}) VALUES ({placeholders})"
|
||||
db.execute_update(sql, tuple(values))
|
||||
|
|
|
|||
|
|
@ -282,11 +282,15 @@
|
|||
color: #e74c3c;
|
||||
}
|
||||
|
||||
/* 表格横向滚动:避免整页过宽,内容区域可左右滑动 */
|
||||
.table-wrapper {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
margin-top: 1rem;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.table-wrapper::-webkit-scrollbar {
|
||||
|
|
@ -311,7 +315,7 @@
|
|||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
display: none;
|
||||
min-width: 1200px; /* 确保表格有最小宽度,避免列被压缩 */
|
||||
min-width: 1100px; /* 表格最小宽度,超出时由 table-wrapper 横向滚动 */
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
|
@ -323,10 +327,10 @@
|
|||
.trades-table th {
|
||||
background-color: #34495e;
|
||||
color: white;
|
||||
padding: 0.75rem 0.5rem;
|
||||
padding: 0.5rem 0.4rem;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
|
|
@ -334,89 +338,88 @@
|
|||
}
|
||||
|
||||
.trades-table td {
|
||||
padding: 0.6rem 0.5rem;
|
||||
padding: 0.5rem 0.4rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 优化特定列的宽度 */
|
||||
/* 列宽适度收紧,减少横向占用,仍保证可读 */
|
||||
.trades-table th:nth-child(1),
|
||||
.trades-table td:nth-child(1) {
|
||||
min-width: 60px;
|
||||
max-width: 80px;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(2),
|
||||
.trades-table td:nth-child(2) {
|
||||
min-width: 90px;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(3),
|
||||
.trades-table td:nth-child(3) {
|
||||
min-width: 60px;
|
||||
min-width: 52px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(4),
|
||||
.trades-table td:nth-child(4) {
|
||||
min-width: 90px;
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(5),
|
||||
.trades-table td:nth-child(5) {
|
||||
min-width: 90px;
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(6),
|
||||
.trades-table td:nth-child(6) {
|
||||
min-width: 90px;
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(7),
|
||||
.trades-table td:nth-child(7) {
|
||||
min-width: 90px;
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(8),
|
||||
.trades-table td:nth-child(8) {
|
||||
min-width: 90px;
|
||||
min-width: 78px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(9),
|
||||
.trades-table td:nth-child(9) {
|
||||
min-width: 100px;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(10),
|
||||
.trades-table td:nth-child(10) {
|
||||
min-width: 100px;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(11),
|
||||
.trades-table td:nth-child(11) {
|
||||
min-width: 80px;
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(12),
|
||||
.trades-table td:nth-child(12) {
|
||||
min-width: 100px;
|
||||
min-width: 88px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(13),
|
||||
.trades-table td:nth-child(13) {
|
||||
min-width: 200px;
|
||||
min-width: 160px;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(14),
|
||||
.trades-table td:nth-child(14) {
|
||||
min-width: 140px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.trades-table th:nth-child(15),
|
||||
.trades-table td:nth-child(15) {
|
||||
min-width: 140px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.trades-table tr:hover {
|
||||
|
|
|
|||
|
|
@ -81,14 +81,13 @@ const TradeList = () => {
|
|||
setUseCustomDate(false)
|
||||
}
|
||||
|
||||
// 导出当前订单数据
|
||||
// 导出当前订单数据(含入场/离场原因、入场思路等完整字段,便于后续分析)
|
||||
const handleExport = () => {
|
||||
if (trades.length === 0) {
|
||||
alert('暂无数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
// 准备导出数据
|
||||
const exportData = trades.map(trade => {
|
||||
const notional = trade.notional_usdt !== undefined && trade.notional_usdt !== null
|
||||
? parseFloat(trade.notional_usdt)
|
||||
|
|
@ -102,7 +101,7 @@ const TradeList = () => {
|
|||
const pnl = parseFloat(trade.pnl || 0)
|
||||
const pnlPercent = margin > 0 ? (pnl / margin) * 100 : 0
|
||||
|
||||
return {
|
||||
const row = {
|
||||
交易ID: trade.id,
|
||||
交易对: trade.symbol,
|
||||
方向: trade.side,
|
||||
|
|
@ -120,7 +119,21 @@ const TradeList = () => {
|
|||
平仓订单号: trade.exit_order_id || '-',
|
||||
入场时间: trade.entry_time,
|
||||
平仓时间: trade.exit_time || null,
|
||||
// 以下为分析用完整字段
|
||||
入场原因: trade.entry_reason ?? null,
|
||||
离场原因: trade.exit_reason ?? null,
|
||||
持仓时长分钟: trade.duration_minutes ?? null,
|
||||
止损价: trade.stop_loss_price != null ? parseFloat(trade.stop_loss_price) : null,
|
||||
止盈价: trade.take_profit_price != null ? parseFloat(trade.take_profit_price) : null,
|
||||
第一目标止盈价: trade.take_profit_1 != null ? parseFloat(trade.take_profit_1) : null,
|
||||
第二目标止盈价: trade.take_profit_2 != null ? parseFloat(trade.take_profit_2) : null,
|
||||
ATR: trade.atr != null ? parseFloat(trade.atr) : null,
|
||||
策略类型: trade.strategy_type ?? null,
|
||||
}
|
||||
if (trade.entry_context != null) {
|
||||
row.入场思路 = trade.entry_context
|
||||
}
|
||||
return row
|
||||
})
|
||||
|
||||
// 生成文件名
|
||||
|
|
@ -288,7 +301,7 @@ const TradeList = () => {
|
|||
重置
|
||||
</button>
|
||||
{trades.length > 0 && (
|
||||
<button className="btn-export" onClick={handleExport} title="导出当前显示的订单数据">
|
||||
<button className="btn-export" onClick={handleExport} title="导出完整数据(含入场/离场原因、入场思路等),便于后续分析">
|
||||
导出数据 ({trades.length})
|
||||
</button>
|
||||
)}
|
||||
|
|
@ -430,7 +443,8 @@ const TradeList = () => {
|
|||
<div className="no-data">暂无交易记录</div>
|
||||
) : (
|
||||
<>
|
||||
{/* 桌面端表格 */}
|
||||
{/* 桌面端表格:用横向滚动包裹,避免整页过宽 */}
|
||||
<div className="table-wrapper">
|
||||
<table className="trades-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -535,6 +549,7 @@ const TradeList = () => {
|
|||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 移动端卡片 */}
|
||||
<div className="trades-cards">
|
||||
|
|
|
|||
|
|
@ -160,7 +160,8 @@ class PositionManager:
|
|||
trend_4h: Optional[str] = None,
|
||||
atr: Optional[float] = None,
|
||||
klines: Optional[List] = None,
|
||||
bollinger: Optional[Dict] = None
|
||||
bollinger: Optional[Dict] = None,
|
||||
entry_context: Optional[Dict] = None,
|
||||
) -> Optional[Dict]:
|
||||
"""
|
||||
开仓
|
||||
|
|
@ -630,6 +631,7 @@ class PositionManager:
|
|||
atr=atr,
|
||||
notional_usdt=notional_usdt,
|
||||
margin_usdt=margin_usdt,
|
||||
entry_context=entry_context, # 入场思路与过程(便于事后分析策略执行效果)
|
||||
)
|
||||
logger.info(f"✓ {symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -192,6 +192,24 @@ class TradingStrategy:
|
|||
f"(信号强度: {signal_strength}/10)"
|
||||
)
|
||||
|
||||
# 构建「入场思路/过程」并写入订单,便于事后综合分析策略执行效果
|
||||
entry_context = {
|
||||
'signal_strength': signal_strength,
|
||||
'market_regime': market_regime,
|
||||
'trend_4h': trade_signal.get('trend_4h'),
|
||||
'change_percent': change_percent,
|
||||
'direction': trade_direction,
|
||||
'reason': entry_reason,
|
||||
'rsi': symbol_info.get('rsi'),
|
||||
'volume_confirmed': True, # 已通过 _check_volume_confirmation
|
||||
'filters_passed': ['only_trending', 'should_trade', 'volume_ok', 'signal_ok'],
|
||||
}
|
||||
macd_hist = symbol_info.get('macd', {}).get('histogram') if isinstance(symbol_info.get('macd'), dict) else None
|
||||
if macd_hist is not None:
|
||||
entry_context['macd_histogram'] = macd_hist
|
||||
if symbol_info.get('atr') is not None:
|
||||
entry_context['atr'] = symbol_info.get('atr')
|
||||
|
||||
# 开仓(使用改进的仓位管理)
|
||||
position = await self.position_manager.open_position(
|
||||
symbol=symbol,
|
||||
|
|
@ -204,7 +222,8 @@ class TradingStrategy:
|
|||
trend_4h=trade_signal.get('trend_4h'),
|
||||
atr=symbol_info.get('atr'),
|
||||
klines=symbol_info.get('klines'), # 传递K线数据用于动态止损
|
||||
bollinger=symbol_info.get('bollinger') # 传递布林带数据用于动态止损
|
||||
bollinger=symbol_info.get('bollinger'), # 传递布林带数据用于动态止损
|
||||
entry_context=entry_context,
|
||||
)
|
||||
|
||||
if position:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user