better fees calc

This commit is contained in:
Dobromir Popov
2025-05-28 15:23:15 +03:00
parent 398aca32ad
commit de01d3665c
5 changed files with 290 additions and 149 deletions

View File

@ -649,11 +649,17 @@ class TradingDashboard:
prevent_initial_call=True
)
def clear_trade_history(n_clicks):
"""Clear the closed trades history"""
"""Clear trade history and reset session stats"""
if n_clicks and n_clicks > 0:
self.clear_closed_trades_history()
return [html.P("Trade history cleared", className="text-muted text-center")]
return self._create_closed_trades_table()
try:
# Clear both closed trades and session stats (they're the same now)
self.clear_closed_trades_history()
logger.info("DASHBOARD: Trade history and session stats cleared by user")
return [html.P("Trade history cleared", className="text-success text-center")]
except Exception as e:
logger.error(f"Error clearing trade history: {e}")
return [html.P(f"Error clearing history: {str(e)}", className="text-danger text-center")]
return dash.no_update
def _simulate_price_update(self, symbol: str, base_price: float) -> float:
"""
@ -1407,14 +1413,37 @@ class TradingDashboard:
return None
def _process_trading_decision(self, decision: Dict) -> None:
"""Process a trading decision and update PnL tracking with position flipping"""
"""Process a trading decision and update PnL tracking with enhanced fee calculation"""
try:
if not decision:
return
current_time = datetime.now(timezone.utc) # Use UTC for consistency
fee_rate = 0.001 # 0.1% trading fee
fee_rate = 0.0 # 0% PROMO FEE (Current, but temporary)
# Get fee structure from config (fallback to hardcoded values)
try:
from core.config import get_config
config = get_config()
trading_fees = config.get('trading', {}).get('trading_fees', {})
maker_fee_rate = trading_fees.get('maker', 0.0000) # 0.00% maker
taker_fee_rate = trading_fees.get('taker', 0.0005) # 0.05% taker
default_fee_rate = trading_fees.get('default', 0.0005) # 0.05% default
except:
# Fallback to hardcoded asymmetrical fees
maker_fee_rate = 0.0000 # 0.00% maker fee
taker_fee_rate = 0.0005 # 0.05% taker fee
default_fee_rate = 0.0005 # 0.05% default
# For simulation, assume most trades are taker orders (market orders)
# In real trading, this would be determined by order type
fee_rate = taker_fee_rate # Default to taker fee
fee_type = 'taker' # Default to taker
# If using limit orders that get filled (maker), use maker fee
# This could be enhanced based on actual order execution data
if decision.get('order_type') == 'limit' and decision.get('filled_as_maker', False):
fee_rate = maker_fee_rate
fee_type = 'maker'
# Execute trade through MEXC if available
mexc_success = False
@ -1479,6 +1508,8 @@ class TradingDashboard:
close_record['entry_price'] = entry_price
close_record['pnl'] = net_pnl
close_record['fees'] = fee
close_record['fee_type'] = fee_type
close_record['fee_rate'] = fee_rate
close_record['size'] = size # Use original position size for close
self.session_trades.append(close_record)
@ -1493,6 +1524,8 @@ class TradingDashboard:
'size': size,
'gross_pnl': gross_pnl,
'fees': fee + self.current_position['fees'],
'fee_type': fee_type,
'fee_rate': fee_rate,
'net_pnl': net_pnl,
'duration': current_time - entry_time,
'symbol': decision.get('symbol', 'ETH/USDT'),
@ -1527,6 +1560,8 @@ class TradingDashboard:
trade_record = decision.copy()
trade_record['position_action'] = 'OPEN_LONG'
trade_record['fees'] = fee
trade_record['fee_type'] = fee_type
trade_record['fee_rate'] = fee_rate
self.session_trades.append(trade_record)
logger.info(f"[TRADE] OPENED LONG: {decision['size']:.6f} (${decision.get('usd_size', 0.1):.2f}) @ ${decision['price']:.2f} (confidence: {decision['confidence']:.1%})")
@ -1556,6 +1591,8 @@ class TradingDashboard:
close_record['entry_price'] = entry_price
close_record['pnl'] = net_pnl
close_record['fees'] = fee
close_record['fee_type'] = fee_type
close_record['fee_rate'] = fee_rate
self.session_trades.append(close_record)
# Add to closed trades accounting list
@ -1569,6 +1606,8 @@ class TradingDashboard:
'size': size,
'gross_pnl': gross_pnl,
'fees': fee + self.current_position['fees'],
'fee_type': fee_type,
'fee_rate': fee_rate,
'net_pnl': net_pnl,
'duration': current_time - entry_time,
'symbol': decision.get('symbol', 'ETH/USDT'),
@ -1610,6 +1649,8 @@ class TradingDashboard:
close_record['entry_price'] = entry_price
close_record['pnl'] = net_pnl
close_record['fees'] = fee
close_record['fee_type'] = fee_type
close_record['fee_rate'] = fee_rate
close_record['size'] = size # Use original position size for close
self.session_trades.append(close_record)
@ -1624,6 +1665,8 @@ class TradingDashboard:
'size': size,
'gross_pnl': gross_pnl,
'fees': fee + self.current_position['fees'],
'fee_type': fee_type,
'fee_rate': fee_rate,
'net_pnl': net_pnl,
'duration': current_time - entry_time,
'symbol': decision.get('symbol', 'ETH/USDT'),
@ -1655,6 +1698,8 @@ class TradingDashboard:
trade_record = decision.copy()
trade_record['position_action'] = 'OPEN_SHORT'
trade_record['fees'] = fee
trade_record['fee_type'] = fee_type
trade_record['fee_rate'] = fee_rate
self.session_trades.append(trade_record)
logger.info(f"[TRADE] OPENED SHORT: {decision['size']:.6f} (${decision.get('usd_size', 0.1):.2f}) @ ${decision['price']:.2f} (confidence: {decision['confidence']:.1%})")
@ -1781,7 +1826,7 @@ class TradingDashboard:
logger.info("[ORCHESTRATOR] Trading loop started in background")
def _create_closed_trades_table(self) -> List:
"""Create closed trades history table with persistence"""
"""Create simplified closed trades history table focusing on total fees per closed position"""
try:
if not self.closed_trades:
return [html.P("No closed trades yet", className="text-muted text-center")]
@ -1805,6 +1850,9 @@ class TradingDashboard:
position_size = trade.get('size', 0)
size_display = f"{position_size:.4f}" if position_size < 1 else f"{position_size:.2f}"
# Simple total fees display
total_fees = trade.get('fees', 0)
table_rows.append(
html.Tr([
html.Td(f"#{trade['trade_id']}", className="small"),
@ -1812,7 +1860,7 @@ class TradingDashboard:
html.Td(size_display, className="small text-info"),
html.Td(f"${trade['entry_price']:.2f}", className="small"),
html.Td(f"${trade['exit_price']:.2f}", className="small"),
html.Td(f"${trade.get('fees', 0):.2f}", className="small text-warning"),
html.Td(f"${total_fees:.3f}", className="small text-warning"),
html.Td(f"${trade['net_pnl']:.2f}", className="small fw-bold"),
html.Td(duration_str, className="small"),
html.Td("" if trade.get('mexc_executed', False) else "SIM",
@ -1820,7 +1868,7 @@ class TradingDashboard:
], className=row_class)
)
# Create table
# Create simple table
table = html.Table([
html.Thead([
html.Tr([
@ -1829,8 +1877,8 @@ class TradingDashboard:
html.Th("Size", className="small"),
html.Th("Entry", className="small"),
html.Th("Exit", className="small"),
html.Th("Fees", className="small"),
html.Th("P&L", className="small"),
html.Th("Total Fees", className="small"),
html.Th("Net P&L", className="small"),
html.Th("Duration", className="small"),
html.Th("MEXC", className="small")
])
@ -1838,24 +1886,7 @@ class TradingDashboard:
html.Tbody(table_rows)
], className="table table-sm table-striped")
# Add summary stats
total_trades = len(self.closed_trades)
winning_trades = len([t for t in self.closed_trades if t['net_pnl'] > 0])
total_pnl = sum(t['net_pnl'] for t in self.closed_trades)
total_fees_closed = sum(t.get('fees', 0) for t in self.closed_trades)
win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
summary = html.Div([
html.Small([
html.Strong(f"Total: {total_trades} | "),
html.Span(f"Win Rate: {win_rate:.1f}% | ", className="text-info"),
html.Span(f"Fees: ${total_fees_closed:.2f} | ", className="text-warning"),
html.Span(f"Total P&L: ${total_pnl:.2f}",
className="text-success" if total_pnl >= 0 else "text-danger")
], className="d-block mb-2")
])
return [summary, table]
return [table]
except Exception as e:
logger.error(f"Error creating closed trades table: {e}")
@ -1935,88 +1966,95 @@ class TradingDashboard:
logger.error(f"Error clearing closed trades history: {e}")
def _create_session_performance(self) -> List:
"""Create compact session performance display with signal statistics"""
"""Create enhanced session performance display with multiline format and total volume"""
try:
session_duration = datetime.now() - self.session_start
duration_str = f"{session_duration.seconds//3600:02d}:{(session_duration.seconds//60)%60:02d}:{session_duration.seconds%60:02d}"
# Calculate comprehensive session metrics from closed trades
total_trades = len(self.closed_trades)
winning_trades = len([t for t in self.closed_trades if t['net_pnl'] > 0])
total_net_pnl = sum(t['net_pnl'] for t in self.closed_trades)
total_fees_paid = sum(t.get('fees', 0) for t in self.closed_trades)
# Calculate win rate
winning_trades = [t for t in self.session_trades if 'pnl' in t and t['pnl'] > 0]
losing_trades = [t for t in self.session_trades if 'pnl' in t and t['pnl'] < 0]
closed_trades = len(winning_trades) + len(losing_trades)
win_rate = (len(winning_trades) / closed_trades * 100) if closed_trades > 0 else 0
# Calculate total volume (price * size for each trade)
total_volume = 0
for trade in self.closed_trades:
entry_volume = trade.get('entry_price', 0) * trade.get('size', 0)
exit_volume = trade.get('exit_price', 0) * trade.get('size', 0)
total_volume += entry_volume + exit_volume # Both entry and exit contribute to volume
# Calculate signal statistics
executed_signals = [d for d in self.recent_decisions if isinstance(d, dict) and d.get('signal_type') == 'EXECUTED']
ignored_signals = [d for d in self.recent_decisions if isinstance(d, dict) and d.get('signal_type') == 'IGNORED']
total_signals = len(executed_signals) + len(ignored_signals)
execution_rate = (len(executed_signals) / total_signals * 100) if total_signals > 0 else 0
# Calculate fee breakdown
maker_fees = sum(t.get('fees', 0) for t in self.closed_trades if t.get('fee_type') == 'maker')
taker_fees = sum(t.get('fees', 0) for t in self.closed_trades if t.get('fee_type') != 'maker')
# Calculate other metrics
total_volume = sum(t.get('price', 0) * t.get('size', 0) for t in self.session_trades)
avg_trade_pnl = (self.total_realized_pnl / closed_trades) if closed_trades > 0 else 0
portfolio_value = self.starting_balance + self.total_realized_pnl
portfolio_return = (self.total_realized_pnl / self.starting_balance * 100) if self.starting_balance > 0 else 0
# Calculate gross P&L (before fees)
gross_pnl = total_net_pnl + total_fees_paid
performance_items = [
# Row 1: Duration and Portfolio Value
# Calculate rates and percentages
win_rate = (winning_trades / total_trades * 100) if total_trades > 0 else 0
avg_trade_pnl = (total_net_pnl / total_trades) if total_trades > 0 else 0
fee_impact = (total_fees_paid / gross_pnl * 100) if gross_pnl > 0 else 0
fee_percentage_of_volume = (total_fees_paid / total_volume * 100) if total_volume > 0 else 0
# Calculate signal stats from recent decisions
total_signals = len([d for d in self.recent_decisions if d.get('signal')])
executed_signals = len([d for d in self.recent_decisions if d.get('signal') and d.get('executed')])
signal_efficiency = (executed_signals / total_signals * 100) if total_signals > 0 else 0
# Create enhanced multiline performance display
metrics = [
# Line 1: Basic trade statistics
html.Div([
html.Div([
html.Strong("Duration: "),
html.Span(duration_str, className="text-info")
], className="col-6 small"),
html.Div([
html.Strong("Portfolio: "),
html.Span(f"${portfolio_value:,.2f}",
className="text-success" if portfolio_value >= self.starting_balance else "text-danger")
], className="col-6 small")
], className="row mb-1"),
html.Small([
html.Strong(f"Total: {total_trades} trades | "),
html.Span(f"Win Rate: {win_rate:.1f}% | ", className="text-info"),
html.Span(f"Avg P&L: ${avg_trade_pnl:.2f}",
className="text-success" if avg_trade_pnl >= 0 else "text-danger")
])
], className="mb-1"),
# Row 2: Trades and Win Rate
# Line 2: P&L breakdown (Gross vs Net)
html.Div([
html.Div([
html.Strong("Trades: "),
html.Span(f"{len(self.session_trades)}", className="text-info")
], className="col-6 small"),
html.Div([
html.Strong("Win Rate: "),
html.Span(f"{win_rate:.1f}%",
className="text-success" if win_rate >= 50 else "text-warning")
], className="col-6 small")
], className="row mb-1"),
html.Small([
html.Strong("P&L: "),
html.Span(f"Gross: ${gross_pnl:.2f} | ",
className="text-success" if gross_pnl >= 0 else "text-danger"),
html.Span(f"Net: ${total_net_pnl:.2f} | ",
className="text-success" if total_net_pnl >= 0 else "text-danger"),
html.Span(f"Fee Impact: {fee_impact:.1f}%", className="text-warning")
])
], className="mb-1"),
# Row 3: Signals and Execution Rate
# Line 3: Fee breakdown with volume for validation
html.Div([
html.Div([
html.Strong("Signals: "),
html.Span(f"{total_signals}", className="text-info")
], className="col-6 small"),
html.Div([
html.Strong("Exec Rate: "),
html.Span(f"{execution_rate:.1f}%",
className="text-success" if execution_rate >= 30 else "text-warning")
], className="col-6 small")
], className="row mb-1"),
# Row 4: Portfolio Return and Fees
html.Div([
html.Div([
html.Strong("Return: "),
html.Span(f"{portfolio_return:+.2f}%",
className="text-success" if portfolio_return >= 0 else "text-danger")
], className="col-6 small"),
html.Div([
html.Small([
html.Strong("Fees: "),
html.Span(f"${self.total_fees:.2f}", className="text-muted")
], className="col-6 small")
], className="row")
html.Span(f"Total: ${total_fees_paid:.3f} | ", className="text-warning"),
html.Span(f"Maker: ${maker_fees:.3f} (0.00%) | ", className="text-success"),
html.Span(f"Taker: ${taker_fees:.3f} (0.05%)", className="text-danger")
])
], className="mb-1"),
# Line 4: Volume and fee percentage for validation
html.Div([
html.Small([
html.Strong("Volume: "),
html.Span(f"${total_volume:,.0f} | ", className="text-muted"),
html.Strong("Fee %: "),
html.Span(f"{fee_percentage_of_volume:.4f}% | ", className="text-warning"),
html.Strong("Signals: "),
html.Span(f"{executed_signals}/{total_signals} ({signal_efficiency:.1f}%)", className="text-info")
])
], className="mb-2")
]
return performance_items
return metrics
except Exception as e:
logger.error(f"Error creating session performance: {e}")
return [html.P(f"Error: {str(e)}", className="text-danger")]
return [html.Div([
html.Strong("Session Performance", className="text-primary"),
html.Br(),
html.Small(f"Error loading metrics: {str(e)}", className="text-danger")
])]
def _force_demo_signal(self, symbol: str, current_price: float) -> None:
"""DISABLED - No demo signals, only real market data"""