"""Paper trading engine for OpenClaw trading system.

This module provides a PaperTradingEngine class that simulates real trading
conditions without risking actual funds. It supports:
- Real-time market data via external price feeds (e.g., CCXTAdapter)
- Market and limit order simulation with realistic matching
- Configurable trading fees (maker/taker)
- Slippage simulation
- Position tracking with unrealized PnL
- Realized PnL calculation
- Comprehensive trade history and reporting
"""

import asyncio
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal, ROUND_DOWN
from typing import Dict, List, Optional, Callable, Any
from enum import Enum

from openclaw.exchange.base import Exchange, ExchangeError, InsufficientFundsError, InvalidOrderError
from openclaw.exchange.models import (
    Balance,
    Order,
    OrderSide,
    OrderStatus,
    OrderType,
    Position,
    Ticker,
)
from openclaw.utils.logging import get_logger


class PaperTradingError(ExchangeError):
    """Exception for paper trading specific errors."""
    pass


class PriceFeedError(PaperTradingError):
    """Raised when price feed data is unavailable or invalid."""
    pass


@dataclass
class Trade:
    """Record of a completed trade.

    Attributes:
        trade_id: Unique trade identifier
        order_id: Reference to parent order
        symbol: Trading symbol
        side: Buy or sell
        amount: Trade quantity
        price: Execution price
        fee: Trading fee paid
        fee_asset: Asset in which fee was paid
        timestamp: Trade execution time
        realized_pnl: Realized profit/loss (for closing trades)
    """
    trade_id: str
    order_id: str
    symbol: str
    side: OrderSide
    amount: float
    price: float
    fee: float
    fee_asset: str
    timestamp: datetime
    realized_pnl: float = 0.0


@dataclass
class FeeConfig:
    """Trading fee configuration.

    Attributes:
        maker_fee: Maker fee rate (e.g., 0.001 = 0.1%)
        taker_fee: Taker fee rate (e.g., 0.001 = 0.1%)
    """
    maker_fee: float = 0.001
    taker_fee: float = 0.001


@dataclass
class SlippageConfig:
    """Slippage simulation configuration.

    Attributes:
        enabled: Whether slippage is enabled
        base_slippage_pct: Base slippage percentage
        volatility_factor: How much market volatility affects slippage
        volume_factor: How much volume affects slippage
    """
    enabled: bool = True
    base_slippage_pct: float = 0.05
    volatility_factor: float = 0.5
    volume_factor: float = 0.3


@dataclass
class PositionSummary:
    """Summary of all positions.

    Attributes:
        total_positions: Number of open positions
        total_unrealized_pnl: Total unrealized PnL across all positions
        total_unrealized_pnl_pct: Total unrealized PnL percentage
        total_market_value: Total market value of all positions
    """
    total_positions: int
    total_unrealized_pnl: float
    total_unrealized_pnl_pct: float
    total_market_value: float


@dataclass
class AccountSummary:
    """Complete account summary for reporting.

    Attributes:
        timestamp: Summary generation time
        initial_capital: Starting capital
        available_capital: Currently available for trading
        total_balance: Total account value (available + positions)
        total_realized_pnl: Total realized PnL from closed trades
        total_unrealized_pnl: Total unrealized PnL from open positions
        total_return_pct: Total return percentage
        open_positions: Number of open positions
        total_trades: Total number of executed trades
        win_rate: Percentage of profitable trades
        avg_trade_return: Average return per trade
    """
    timestamp: datetime
    initial_capital: float
    available_capital: float
    total_balance: float
    total_realized_pnl: float
    total_unrealized_pnl: float
    total_return_pct: float
    open_positions: int
    total_trades: int
    win_rate: float
    avg_trade_return: float


class PaperTradingEngine:
    """Paper trading engine for realistic trading simulation.

    Simulates trading with real market data while tracking:
    - Account balances and positions
    - Order matching and execution
    - Trading fees and slippage
    - Realized and unrealized PnL
    - Complete trade history

    Args:
        name: Engine name identifier
        initial_capital: Initial capital allocation {asset: amount}
        fee_config: Fee configuration
        slippage_config: Slippage simulation config
        price_feed: Optional async callable to get current prices
    """

    def __init__(
        self,
        name: str = "paper_trading",
        initial_capital: Optional[Dict[str, float]] = None,
        fee_config: Optional[FeeConfig] = None,
        slippage_config: Optional[SlippageConfig] = None,
        price_feed: Optional[Callable[[str], Any]] = None,
    ):
        self.name = name
        self.logger = get_logger(f"paper_trading.{name}")

        # Configuration
        self.fee_config = fee_config or FeeConfig()
        self.slippage_config = slippage_config or SlippageConfig()
        self._price_feed = price_feed

        # Account state
        self._initial_capital = initial_capital or {"USDT": 10000.0}
        self._balances: Dict[str, Balance] = {}
        self._initialize_balances()

        # Orders and positions
        self._orders: Dict[str, Order] = {}
        self._positions: Dict[str, Position] = {}
        self._trades: List[Trade] = []

        # Pending limit orders
        self._pending_orders: Dict[str, Order] = {}

        # Tracking
        self._total_realized_pnl: float = 0.0
        self._winning_trades: int = 0
        self._losing_trades: int = 0

        # Price cache
        self._price_cache: Dict[str, float] = {}

        self.logger.info(
            f"PaperTradingEngine initialized with capital: {self._initial_capital}"
        )

    def _initialize_balances(self) -> None:
        """Initialize account balances from initial capital."""
        for asset, amount in self._initial_capital.items():
            self._balances[asset.upper()] = Balance(
                asset=asset.upper(),
                free=float(amount),
                locked=0.0
            )

    @property
    def balances(self) -> Dict[str, Balance]:
        """Get current balances."""
        return self._balances.copy()

    @property
    def positions(self) -> Dict[str, Position]:
        """Get current positions."""
        return self._positions.copy()

    @property
    def trades(self) -> List[Trade]:
        """Get trade history."""
        return self._trades.copy()

    @property
    def total_realized_pnl(self) -> float:
        """Get total realized PnL."""
        return self._total_realized_pnl

    def _get_base_quote(self, symbol: str) -> tuple[str, str]:
        """Extract base and quote assets from symbol."""
        if "/" in symbol:
            base, quote = symbol.split("/", 1)
            return base.upper(), quote.upper()
        return symbol.upper(), "USD"

    def _generate_order_id(self) -> str:
        """Generate unique order ID."""
        return f"paper_{str(uuid.uuid4())[:12]}"

    def _generate_trade_id(self) -> str:
        """Generate unique trade ID."""
        return f"trade_{str(uuid.uuid4())[:12]}"

    def _get_price(self, symbol: str) -> Optional[float]:
        """Get current price for symbol.

        Priority:
        1. Price feed (if configured)
        2. Price cache
        3. None (if unavailable)
        """
        symbol_upper = symbol.upper()

        # Try price feed first
        if self._price_feed is not None:
            try:
                price = self._price_feed(symbol_upper)
                if asyncio.iscoroutine(price):
                    # Can't run async here, use cache
                    pass
                else:
                    self._price_cache[symbol_upper] = float(price)
                    return float(price)
            except Exception as e:
                self.logger.warning(f"Price feed error for {symbol}: {e}")

        # Fall back to cache
        return self._price_cache.get(symbol_upper)

    async def update_price(self, symbol: str, price: float) -> None:
        """Update price for a symbol and process pending orders.

        Args:
            symbol: Trading symbol
            price: Current market price
        """
        symbol_upper = symbol.upper()
        self._price_cache[symbol_upper] = price

        # Update positions with new price
        if symbol_upper in self._positions:
            self._positions[symbol_upper].current_price = price

        # Check pending limit orders
        await self._process_pending_orders(symbol_upper, price)

    async def _process_pending_orders(self, symbol: str, price: float) -> None:
        """Process pending limit orders against new price."""
        to_fill = []

        for order_id, order in self._pending_orders.items():
            if order.symbol.upper() != symbol.upper():
                continue

            if order.order_type == OrderType.LIMIT and order.price:
                if order.side == OrderSide.BUY and price <= order.price:
                    # Buy limit order can be filled
                    to_fill.append(order_id)
                elif order.side == OrderSide.SELL and price >= order.price:
                    # Sell limit order can be filled
                    to_fill.append(order_id)

        for order_id in to_fill:
            await self._fill_order(order_id, price)

    def _calculate_slippage(self, price: float, side: OrderSide, amount: float) -> float:
        """Calculate slippage-adjusted price."""
        if not self.slippage_config.enabled:
            return price

        # Base slippage
        slippage_pct = self.slippage_config.base_slippage_pct

        # Add randomness for realism
        import random
        random_factor = random.uniform(0.5, 1.5)
        slippage_pct *= random_factor

        # Apply slippage
        slippage = price * (slippage_pct / 100)
        if side == OrderSide.BUY:
            return price + slippage
        return price - slippage

    def _calculate_fee(self, amount: float, price: float, is_maker: bool = False) -> float:
        """Calculate trading fee."""
        trade_value = amount * price
        fee_rate = self.fee_config.maker_fee if is_maker else self.fee_config.taker_fee
        return trade_value * fee_rate

    def _check_balance(self, asset: str, required: float) -> bool:
        """Check if sufficient balance exists."""
        balance = self._balances.get(asset.upper())
        if not balance:
            return required <= 0
        return balance.free >= required

    def _lock_balance(self, asset: str, amount: float) -> None:
        """Lock balance for pending order."""
        asset_upper = asset.upper()
        balance = self._balances.get(asset_upper)
        if balance:
            balance.free -= amount
            balance.locked += amount

    def _unlock_balance(self, asset: str, amount: float) -> None:
        """Unlock balance when order is cancelled or filled."""
        asset_upper = asset.upper()
        balance = self._balances.get(asset_upper)
        if balance:
            balance.locked -= amount
            balance.free += amount

    def _deduct_balance(self, asset: str, amount: float) -> None:
        """Deduct from free balance."""
        asset_upper = asset.upper()
        if asset_upper not in self._balances:
            self._balances[asset_upper] = Balance(asset=asset_upper, free=0.0, locked=0.0)
        self._balances[asset_upper].free -= amount

    def _add_balance(self, asset: str, amount: float) -> None:
        """Add to free balance."""
        asset_upper = asset.upper()
        if asset_upper not in self._balances:
            self._balances[asset_upper] = Balance(asset=asset_upper, free=0.0, locked=0.0)
        self._balances[asset_upper].free += amount

    async def place_market_order(
        self,
        symbol: str,
        side: OrderSide,
        amount: float,
    ) -> Order:
        """Place a market order (immediate execution).

        Args:
            symbol: Trading symbol (e.g., "BTC/USDT")
            side: Buy or sell
            amount: Order quantity

        Returns:
            Filled order object

        Raises:
            PriceFeedError: If no price available
            InsufficientFundsError: If insufficient balance
        """
        base_asset, quote_asset = self._get_base_quote(symbol)

        # Get current price
        price = self._get_price(symbol)
        if price is None:
            raise PriceFeedError(f"No price available for {symbol}")

        # Apply slippage
        executed_price = self._calculate_slippage(price, side, amount)

        # Calculate costs
        quote_amount = executed_price * amount
        fee = self._calculate_fee(amount, executed_price, is_maker=False)
        total_cost = quote_amount + fee

        # Check balance
        if side == OrderSide.BUY:
            if not self._check_balance(quote_asset, total_cost):
                available = self._balances.get(quote_asset, Balance(asset=quote_asset)).free
                raise InsufficientFundsError(
                    f"Insufficient {quote_asset} balance: {available:.4f} < {total_cost:.4f}"
                )
        else:
            if not self._check_balance(base_asset, amount):
                available = self._balances.get(base_asset, Balance(asset=base_asset)).free
                raise InsufficientFundsError(
                    f"Insufficient {base_asset} balance: {available:.4f} < {amount:.4f}"
                )

        # Create order
        order_id = self._generate_order_id()
        order = Order(
            order_id=order_id,
            symbol=symbol.upper(),
            side=side,
            order_type=OrderType.MARKET,
            amount=amount,
            price=executed_price,
            status=OrderStatus.FILLED,
            filled_amount=amount,
        )

        # Execute trade
        realized_pnl = await self._execute_trade(
            order=order,
            base_asset=base_asset,
            quote_asset=quote_asset,
            fee=fee,
        )

        self._orders[order_id] = order

        self.logger.info(
            f"Market order filled: {side.value} {amount} {symbol} @ {executed_price:.4f} "
            f"(fee: {fee:.4f}, pnl: {realized_pnl:.4f})"
        )

        return order

    async def place_limit_order(
        self,
        symbol: str,
        side: OrderSide,
        amount: float,
        price: float,
    ) -> Order:
        """Place a limit order (pending execution).

        Args:
            symbol: Trading symbol
            side: Buy or sell
            amount: Order quantity
            price: Limit price

        Returns:
            Pending order object

        Raises:
            InsufficientFundsError: If insufficient balance
            InvalidOrderError: If price is invalid
        """
        if price <= 0:
            raise InvalidOrderError("Limit price must be positive")

        base_asset, quote_asset = self._get_base_quote(symbol)

        # Calculate required balance (lock funds)
        quote_amount = price * amount
        fee = self._calculate_fee(amount, price, is_maker=True)

        if side == OrderSide.BUY:
            total_required = quote_amount + fee
            if not self._check_balance(quote_asset, total_required):
                available = self._balances.get(quote_asset, Balance(asset=quote_asset)).free
                raise InsufficientFundsError(
                    f"Insufficient {quote_asset} balance: {available:.4f} < {total_required:.4f}"
                )
            self._lock_balance(quote_asset, total_required)
        else:
            if not self._check_balance(base_asset, amount):
                available = self._balances.get(base_asset, Balance(asset=base_asset)).free
                raise InsufficientFundsError(
                    f"Insufficient {base_asset} balance: {available:.4f} < {amount:.4f}"
                )
            self._lock_balance(base_asset, amount)

        # Create order
        order_id = self._generate_order_id()
        order = Order(
            order_id=order_id,
            symbol=symbol.upper(),
            side=side,
            order_type=OrderType.LIMIT,
            amount=amount,
            price=price,
            status=OrderStatus.OPEN,
            filled_amount=0.0,
        )

        self._orders[order_id] = order
        self._pending_orders[order_id] = order

        self.logger.info(
            f"Limit order placed: {side.value} {amount} {symbol} @ {price:.4f}"
        )

        return order

    async def _fill_order(self, order_id: str, price: float) -> None:
        """Fill a pending order at the given price."""
        if order_id not in self._pending_orders:
            return

        order = self._pending_orders.pop(order_id)
        base_asset, quote_asset = self._get_base_quote(order.symbol)

        # Apply minimal slippage for limit orders (maker)
        executed_price = price
        fee = self._calculate_fee(order.amount, executed_price, is_maker=True)

        # Update order status
        order.status = OrderStatus.FILLED
        order.filled_amount = order.amount
        order.price = executed_price
        order.updated_at = datetime.now()

        # Execute trade (handles balance updates directly from locked)
        realized_pnl = await self._execute_trade(
            order=order,
            base_asset=base_asset,
            quote_asset=quote_asset,
            fee=fee,
            from_locked=True,
        )

        self.logger.info(
            f"Limit order filled: {order.side.value} {order.amount} {order.symbol} "
            f"@ {executed_price:.4f} (fee: {fee:.4f}, pnl: {realized_pnl:.4f})"
        )

    async def _execute_trade(
        self,
        order: Order,
        base_asset: str,
        quote_asset: str,
        fee: float,
        from_locked: bool = False,
    ) -> float:
        """Execute trade and update balances, positions, and PnL.

        Args:
            order: The order being executed
            base_asset: Base asset symbol
            quote_asset: Quote asset symbol
            fee: Trading fee
            from_locked: Whether to deduct from locked balance (limit orders)

        Returns:
            Realized PnL from this trade
        """
        executed_price = order.price or 0
        amount = order.amount
        side = order.side
        symbol = order.symbol.upper()

        realized_pnl = 0.0

        # Update balances
        if side == OrderSide.BUY:
            # For buy orders: deduct quote, add base
            quote_cost = executed_price * amount

            if from_locked:
                # Deduct from locked balance (limit orders already locked funds)
                # The locked amount was based on limit price, adjust for actual price
                limit_price = order.price or executed_price
                locked_amount = limit_price * amount + self._calculate_fee(amount, limit_price, is_maker=True)
                balance = self._balances.get(quote_asset.upper())
                if balance:
                    balance.locked -= locked_amount
                    # Any excess goes back to free
                    excess = locked_amount - quote_cost - fee
                    if excess > 0:
                        balance.free += excess
            else:
                # Deduct from free balance (market orders)
                total_deduct = quote_cost + fee
                self._deduct_balance(quote_asset, total_deduct)

            # Add base asset
            self._add_balance(base_asset, amount)
        else:
            # For sell orders: deduct base, add quote
            if from_locked:
                # Deduct from locked balance (limit orders)
                balance = self._balances.get(base_asset.upper())
                if balance:
                    balance.locked -= amount
            else:
                # Deduct from free balance (market orders)
                self._deduct_balance(base_asset, amount)

            # Add quote asset (minus fee)
            total_add = executed_price * amount - fee
            self._add_balance(quote_asset, total_add)

        # Update or create position
        if symbol in self._positions:
            position = self._positions[symbol]
            if position.side == side:
                # Adding to position - average entry price
                total_cost = (position.amount * position.entry_price) + (amount * executed_price)
                position.amount += amount
                position.entry_price = total_cost / position.amount
                position.current_price = executed_price
            else:
                # Reducing or closing position
                if amount >= position.amount:
                    # Closing position (possibly reversing)
                    realized_pnl = self._calculate_realized_pnl(
                        position, position.amount, executed_price
                    )
                    remaining = amount - position.amount

                    if remaining > 0:
                        # Reverse position
                        position.side = side
                        position.amount = remaining
                        position.entry_price = executed_price
                        position.current_price = executed_price
                    else:
                        # Fully closed
                        del self._positions[symbol]
                else:
                    # Partial close
                    realized_pnl = self._calculate_realized_pnl(
                        position, amount, executed_price
                    )
                    position.amount -= amount
                    position.current_price = executed_price
        else:
            # New position
            if side == OrderSide.BUY:
                self._positions[symbol] = Position(
                    symbol=symbol,
                    side=side,
                    amount=amount,
                    entry_price=executed_price,
                    current_price=executed_price,
                )
            # Note: Short selling without position is not supported in this implementation

        # Track PnL
        self._total_realized_pnl += realized_pnl
        if realized_pnl > 0:
            self._winning_trades += 1
        elif realized_pnl < 0:
            self._losing_trades += 1

        # Record trade
        trade = Trade(
            trade_id=self._generate_trade_id(),
            order_id=order.order_id,
            symbol=symbol,
            side=side,
            amount=amount,
            price=executed_price,
            fee=fee,
            fee_asset=quote_asset,
            timestamp=datetime.now(),
            realized_pnl=realized_pnl,
        )
        self._trades.append(trade)

        return realized_pnl

    def _calculate_realized_pnl(
        self,
        position: Position,
        amount: float,
        exit_price: float,
    ) -> float:
        """Calculate realized PnL when closing part of a position."""
        price_diff = exit_price - position.entry_price
        if position.side == OrderSide.SELL:
            price_diff = -price_diff
        return price_diff * amount

    async def cancel_order(self, order_id: str) -> bool:
        """Cancel a pending order.

        Args:
            order_id: Order ID to cancel

        Returns:
            True if cancelled, False if not found or already filled
        """
        if order_id not in self._pending_orders:
            return False

        order = self._pending_orders.pop(order_id)
        order.status = OrderStatus.CANCELLED
        order.updated_at = datetime.now()

        # Unlock locked funds
        base_asset, quote_asset = self._get_base_quote(order.symbol)

        if order.side == OrderSide.BUY:
            quote_amount = (order.price or 0) * order.amount
            fee = self._calculate_fee(order.amount, order.price or 0, is_maker=True)
            self._unlock_balance(quote_asset, quote_amount + fee)
        else:
            self._unlock_balance(base_asset, order.amount)

        self.logger.info(f"Order cancelled: {order_id}")
        return True

    def get_order(self, order_id: str) -> Optional[Order]:
        """Get order by ID."""
        return self._orders.get(order_id)

    def get_open_orders(self, symbol: Optional[str] = None) -> List[Order]:
        """Get open (pending) orders."""
        orders = list(self._pending_orders.values())
        if symbol:
            symbol_upper = symbol.upper()
            orders = [o for o in orders if o.symbol == symbol_upper]
        return orders

    def get_balance(self, asset: Optional[str] = None) -> List[Balance]:
        """Get account balances."""
        if asset:
            asset_upper = asset.upper()
            if asset_upper in self._balances:
                return [self._balances[asset_upper]]
            return []
        return list(self._balances.values())

    def get_position(self, symbol: str) -> Optional[Position]:
        """Get position for a symbol."""
        return self._positions.get(symbol.upper())

    def get_positions(self, symbol: Optional[str] = None) -> List[Position]:
        """Get all positions or filter by symbol."""
        if symbol:
            pos = self._positions.get(symbol.upper())
            return [pos] if pos else []
        return list(self._positions.values())

    def get_unrealized_pnl(self, symbol: Optional[str] = None) -> float:
        """Calculate total unrealized PnL."""
        positions = self.get_positions(symbol)
        return sum(p.unrealized_pnl for p in positions if p.current_price)

    def get_position_summary(self) -> PositionSummary:
        """Get summary of all positions."""
        positions = self.get_positions()
        total_market_value = sum(p.market_value for p in positions)

        total_unrealized_pnl = 0.0
        total_cost = 0.0
        for p in positions:
            total_unrealized_pnl += p.unrealized_pnl
            total_cost += p.entry_price * p.amount

        total_unrealized_pnl_pct = 0.0
        if total_cost > 0:
            total_unrealized_pnl_pct = (total_unrealized_pnl / total_cost) * 100

        return PositionSummary(
            total_positions=len(positions),
            total_unrealized_pnl=total_unrealized_pnl,
            total_unrealized_pnl_pct=total_unrealized_pnl_pct,
            total_market_value=total_market_value,
        )

    def get_account_summary(self) -> AccountSummary:
        """Get complete account summary."""
        # Calculate total balance
        position_summary = self.get_position_summary()
        available_capital = sum(b.free for b in self._balances.values())
        total_balance = available_capital + position_summary.total_market_value

        # Calculate initial capital value
        initial_capital_value = sum(self._initial_capital.values())

        # Calculate returns
        total_return = total_balance - initial_capital_value
        total_return_pct = 0.0
        if initial_capital_value > 0:
            total_return_pct = (total_return / initial_capital_value) * 100

        # Calculate win rate
        total_closed_trades = self._winning_trades + self._losing_trades
        win_rate = 0.0
        if total_closed_trades > 0:
            win_rate = (self._winning_trades / total_closed_trades) * 100

        # Average trade return
        avg_trade_return = 0.0
        if self._trades:
            avg_trade_return = sum(t.realized_pnl for t in self._trades) / len(self._trades)

        return AccountSummary(
            timestamp=datetime.now(),
            initial_capital=initial_capital_value,
            available_capital=available_capital,
            total_balance=total_balance,
            total_realized_pnl=self._total_realized_pnl,
            total_unrealized_pnl=position_summary.total_unrealized_pnl,
            total_return_pct=total_return_pct,
            open_positions=position_summary.total_positions,
            total_trades=len(self._trades),
            win_rate=win_rate,
            avg_trade_return=avg_trade_return,
        )

    def get_trade_history(
        self,
        symbol: Optional[str] = None,
        start_time: Optional[datetime] = None,
        end_time: Optional[datetime] = None,
    ) -> List[Trade]:
        """Get filtered trade history.

        Args:
            symbol: Filter by symbol
            start_time: Filter trades after this time
            end_time: Filter trades before this time
        """
        trades = self._trades.copy()

        if symbol:
            symbol_upper = symbol.upper()
            trades = [t for t in trades if t.symbol == symbol_upper]

        if start_time:
            trades = [t for t in trades if t.timestamp >= start_time]

        if end_time:
            trades = [t for t in trades if t.timestamp <= end_time]

        return trades

    def generate_report(self) -> str:
        """Generate text report of trading activity."""
        summary = self.get_account_summary()
        position_summary = self.get_position_summary()

        lines = [
            "=" * 60,
            f"Paper Trading Report - {self.name}",
            f"Generated: {summary.timestamp.strftime('%Y-%m-%d %H:%M:%S')}",
            "=" * 60,
            "",
            "Account Summary:",
            f"  Initial Capital:    {summary.initial_capital:,.2f}",
            f"  Available Capital:  {summary.available_capital:,.2f}",
            f"  Total Balance:      {summary.total_balance:,.2f}",
            f"  Total Return:       {summary.total_return_pct:+.2f}%",
            "",
            "PnL Summary:",
            f"  Realized PnL:       {summary.total_realized_pnl:+,.2f}",
            f"  Unrealized PnL:     {summary.total_unrealized_pnl:+,.2f}",
            "",
            "Trading Statistics:",
            f"  Total Trades:       {summary.total_trades}",
            f"  Open Positions:     {summary.open_positions}",
            f"  Win Rate:           {summary.win_rate:.1f}%",
            f"  Avg Trade Return:   {summary.avg_trade_return:+.4f}",
            "",
        ]

        if self._positions:
            lines.extend([
                "Open Positions:",
                "-" * 60,
            ])
            for pos in self._positions.values():
                pnl_str = f"{pos.unrealized_pnl:+.2f} ({pos.unrealized_pnl_pct:+.2f}%)"
                lines.append(
                    f"  {pos.symbol}: {pos.side.value.upper()} {pos.amount:.6f} "
                    f"@ {pos.entry_price:.4f} (Current: {pos.current_price or 'N/A'}) "
                    f"PnL: {pnl_str}"
                )

        if self._balances:
            lines.extend([
                "",
                "Balances:",
                "-" * 60,
            ])
            for bal in sorted(self._balances.values(), key=lambda x: x.total, reverse=True):
                lines.append(
                    f"  {bal.asset}: {bal.free:.6f} free, {bal.locked:.6f} locked "
                    f"(Total: {bal.total:.6f})"
                )

        lines.append("=" * 60)

        return "\n".join(lines)

    def reset(self) -> None:
        """Reset engine to initial state."""
        self._initialize_balances()
        self._orders.clear()
        self._positions.clear()
        self._pending_orders.clear()
        self._trades.clear()
        self._price_cache.clear()

        self._total_realized_pnl = 0.0
        self._winning_trades = 0
        self._losing_trades = 0

        self.logger.info("PaperTradingEngine reset to initial state")

    async def run_with_price_feed(
        self,
        price_feed: Callable[[], Any],
        symbols: List[str],
        interval: float = 1.0,
    ) -> None:
        """Run engine with continuous price feed updates.

        Args:
            price_feed: Async callable that returns {symbol: price} dict
            symbols: List of symbols to track
            interval: Update interval in seconds
        """
        self.logger.info(f"Starting price feed loop for {symbols}")

        try:
            while True:
                try:
                    prices = await price_feed()
                    for symbol in symbols:
                        if symbol in prices:
                            await self.update_price(symbol, prices[symbol])
                except Exception as e:
                    self.logger.error(f"Price feed error: {e}")

                await asyncio.sleep(interval)
        except asyncio.CancelledError:
            self.logger.info("Price feed loop cancelled")
