This commit is contained in:
薇薇安 2026-01-30 11:03:30 +08:00
parent 9490207537
commit 4f21240116
6 changed files with 88 additions and 30 deletions

View File

@ -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 = {

View File

@ -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))

View File

@ -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 {

View File

@ -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">

View File

@ -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:

View File

@ -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: