fix: sua loi eviction dung nham gia cross-symbol trong trade_executor.py

Phat hien (p) khi viet test cho trade_executor: khi kiem tra hybrid
eviction, PnL cua TAT CA cac trade dang mo (o nhieu symbol khac nhau) bi
tinh bang current_price cua tin hieu dang xu ly, thay vi gia thuc cua
tung symbol. Fix bang cach lookup gia moi nhat theo tung
symbol/exchange/timeframe (batched query, cung pattern da dung dung trong
close_stale_trades), ap dung cho ca xep hang loser LAN gia dong lenh cuoi
cung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Le
2026-07-03 22:29:41 +07:00
parent 4ab2dfbe7c
commit 9a0d2ab220
3 changed files with 183 additions and 22 deletions
+40 -3
View File
@@ -15,8 +15,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import AppException
from app.database import async_session_factory
from app.models.candle import Candle
from app.models.exchange import Exchange
from app.models.real_trade import RealTrade
from app.models.signal import HypotheticalTrade, Signal
from app.models.symbol import Symbol
from app.models.user import User
from app.services.audit_service import log_action
@@ -195,9 +198,42 @@ async def execute_signal_trade(
if open_count >= MAX_OPEN_TRADES:
to_evict = open_count - MAX_OPEN_TRADES + 1
# Each open trade may be on a different symbol than the signal
# currently being processed — using `current_price` (that
# signal's price) for all of them would rank PnL against the
# wrong market price. Look up each trade's own latest candle
# price instead (batched by unique symbol/exchange/timeframe).
trade_keys = list({(t.symbol, t.exchange, t.timeframe) for t in all_open_trades})
price_map: dict[tuple[str, str, str], Decimal] = {}
for sym, ex, tf in trade_keys:
price_result = await db.execute(
select(Candle.close)
.select_from(Symbol)
.join(Candle, Candle.symbol_id == Symbol.id)
.join(Exchange, Exchange.id == Symbol.exchange_id)
.where(and_(
Exchange.name == ex,
Symbol.symbol == sym,
Candle.timeframe == tf,
))
.order_by(desc(Candle.timestamp))
.limit(1)
)
row = price_result.first()
if row:
price_map[(sym, ex, tf)] = row[0]
def _price_for(t: HypotheticalTrade) -> Decimal:
if t.symbol == symbol and t.exchange == exchange_name:
return current_price
# Fall back to entry_price (PnL=0, neutral) if no candle
# data is available for this trade's own symbol.
return price_map.get((t.symbol, t.exchange, t.timeframe), t.entry_price)
open_with_pnl = []
for t in all_open_trades:
pnl_val, _pct = _calculate_pnl(t.entry_price, current_price, t.direction, t.quantity)
pnl_val, _pct = _calculate_pnl(t.entry_price, _price_for(t), t.direction, t.quantity)
open_with_pnl.append((t, pnl_val))
losers = [(t, pnl) for t, pnl in open_with_pnl if pnl < 0]
@@ -208,10 +244,11 @@ async def execute_signal_trade(
eviction_candidates = all_open_trades[:to_evict]
for evict_trade in eviction_candidates:
evict_price = _price_for(evict_trade)
pnl, pnl_pct = _calculate_pnl(
evict_trade.entry_price, current_price, evict_trade.direction, evict_trade.quantity
evict_trade.entry_price, evict_price, evict_trade.direction, evict_trade.quantity
)
evict_trade.exit_price = current_price
evict_trade.exit_price = evict_price
evict_trade.exit_time = datetime.now(timezone.utc)
evict_trade.exit_reason = "MAX_LIMIT_EVICT"
evict_trade.pnl = pnl