import re
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from queue import Empty, Queue
from threading import Event, Lock
from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, Union

import numpy as np
import pandas as pd

from qubx.core.exceptions import QueueTimeout
from qubx.core.series import Bar, OrderBook, Quote, Trade, time_as_nsec
from qubx.core.utils import prec_ceil, prec_floor, time_delta_to_str, time_to_str
from qubx.utils.misc import Stopwatch
from qubx.utils.ntp import start_ntp_thread, time_now

if TYPE_CHECKING:
    from qubx.core.interfaces import IStrategyContext

dt_64 = np.datetime64
td_64 = np.timedelta64

OPTION_FILL_AT_SIGNAL_PRICE = "fill_at_signal_price"
OPTION_SIGNAL_PRICE = "signal_price"
OPTION_SKIP_PRICE_CROSS_CONTROL = "skip_price_cross_control"
OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION = "avoid_stop_order_price_validation"

SW = Stopwatch()


@dataclass
class Liquidation:
    time: dt_64
    quantity: float
    price: float
    side: int

    def __repr__(self):
        return f"[{time_to_str(self.time, 'ns')}]\t {self.quantity} @ {self.price} | {self.side}"  # type: ignore


@dataclass
class AggregatedLiquidations:
    time: dt_64
    avg_buy_price: float
    last_buy_price: float
    buy_amount: float
    buy_count: int
    buy_notional: float

    avg_sell_price: float
    last_sell_price: float
    sell_amount: float
    sell_count: int
    sell_notional: float

    def __repr__(self):
        return f"[{time_to_str(self.time, 'ns')}]\t B:{self.buy_amount} @ {self.avg_buy_price} | S:{self.sell_amount} @ {self.avg_sell_price}"  # type: ignore


@dataclass
class FundingRate:
    time: dt_64
    rate: float
    interval: str
    next_funding_time: dt_64
    mark_price: float | None = None
    index_price: float | None = None

    def __repr__(self):
        return f"[{time_to_str(self.time, 'ns')}]\t {self.rate:.5f} ({self.interval})"  # type: ignore


@dataclass
class FundingPayment:
    """
    Represents a funding payment for a perpetual swap position.

    Based on QuestDB schema: timestamp, symbol, funding_rate, funding_interval_hours
    """

    time: dt_64
    funding_rate: float
    funding_interval_hours: int

    def __post_init__(self):
        if abs(self.funding_rate) > 1.0:
            raise ValueError(f"Invalid funding rate: {self.funding_rate} (must be between -1.0 and 1.0)")

        if self.funding_interval_hours <= 0:
            raise ValueError(f"Invalid funding interval: {self.funding_interval_hours} (must be positive)")

    @property
    def funding_rate_apr(self) -> float:
        return self.funding_rate * 365 * 24 / self.funding_interval_hours * 100

    def __repr__(self):
        return f"[{time_to_str(self.time, 'ns')}]\t {self.funding_rate:.5f} ({self.funding_interval_hours}H)"  # type: ignore


@dataclass
class OpenInterest:
    """
    Represents open interest data for a perpetual swap contract.

    Based on QuestDB schema: timestamp, symbol, open_interest, open_interest_usd
    """

    time: dt_64
    symbol: str
    open_interest: float  # Open interest in base asset units
    open_interest_usd: float  # Open interest in USD value

    def __repr__(self):
        return f"[{time_to_str(self.time, 'ns')}]\t {self.symbol} | {self.open_interest:.2f} ({self.open_interest_usd:.4f})"  # type: ignore


@dataclass
class TimestampedDict:
    """
    Generic class for representing arbitrary data (as dict) with timestamp

    TODO: probably we need to have generic interface for classes like Quote, Bar, .... etc
    """

    time: dt_64
    data: dict[str, Any]

    def __getitem__(self, k: str):
        return self.data[k]

    def __repr__(self):
        return f"[{time_to_str(self.time, 'ns')}]\t {str(self.data)}"  # type: ignore


class ITimeProvider:
    """
    Generic interface for providing current time
    """

    def time(self) -> dt_64:
        """
        Returns current time
        """
        ...


# Alias for timestamped data types used in Qubx
Timestamped: TypeAlias = (
    Quote
    | Trade
    | Bar
    | OrderBook
    | TimestampedDict
    | FundingRate
    | Liquidation
    | FundingPayment
    | AggregatedLiquidations
)


@dataclass
class TargetPosition:
    """
    Class for presenting target position calculated from signal
    """

    time: dt_64 | str  # time when position was created
    instrument: "Instrument"
    target_position_size: float  # actual position size after processing in sizer
    entry_price: float | None = None
    stop_price: float | None = None
    take_price: float | None = None
    options: dict[str, Any] = field(default_factory=dict)

    @property
    def price(self) -> float | None:
        return self.entry_price

    @property
    def stop(self) -> float | None:
        return self.stop_price

    @property
    def take(self) -> float | None:
        return self.take_price

    def __str__(self) -> str:
        _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
        _p = f" @ {self.entry_price}" if self.entry_price is not None else ""
        _s = f" stop: {self.stop_price}" if self.stop_price is not None else ""
        _t = f" take: {self.take_price}" if self.take_price is not None else ""
        return f"[{_d}] TARGET {self.target_position_size:+f} {self.instrument.base}{_p}{_s}{_t} for {self.instrument}"


@dataclass
class Signal:
    """
    Class for presenting signals generated by strategy

    Attributes:
        reference_price: float - aux market price when signal was generated
        is_service: bool - when we need this signal only for informative purposes (post-factum risk management etc)

        Options:
            - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
            - group: str - group name for signal
            - comment: str - comment for signal
            - options: dict[str, Any] - additional options for signal
    """

    time: dt_64 | str  # time when signal was generated
    instrument: "Instrument"
    signal: float
    price: float | None = None
    stop: float | None = None
    take: float | None = None
    reference_price: float | None = None
    group: str = ""
    comment: str = ""
    options: dict[str, Any] = field(default_factory=dict)
    is_service: bool = False  # when we need this signal only for informative purposes (post-factum risk management etc)

    def target_for_amount(self, amount: float, **kwargs) -> TargetPosition:
        assert not self.is_service, "Service signals can't be converted to target positions !"
        return self.instrument.target(
            self.time,
            self.instrument.round_size_down(amount),
            entry_price=self.price,
            stop_price=self.stop,
            take_price=self.take,
            options=self.options,
            **kwargs,
        )

    def __str__(self) -> str:
        _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
        _p = f" @ {self.price}" if self.price is not None else ""
        _s = f" stop: {self.stop}" if self.stop is not None else ""
        _t = f" take: {self.take}" if self.take is not None else ""
        _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
        _c = f" ({self.comment})" if self.comment else ""
        _i = "SERVICE ::" if self.is_service else ""

        return f"[{_d}] {_i}{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"

    def copy(self) -> "Signal":
        """
        Return a copy of the original signal
        """
        return Signal(
            self.time,
            self.instrument,
            self.signal,
            self.price,
            self.stop,
            self.take,
            self.reference_price,
            self.group,
            self.comment,
            dict(self.options),
            self.is_service,
        )


@dataclass
class InitializingSignal(Signal):
    """
    Special signal type for post-warmup initialization
    """

    use_limit_order: bool = False  # if True, then use limit order for post-warmup initialization

    def __str__(self) -> str:
        _d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
        _p = f" @ {self.price}" if self.price is not None else ""
        _s = f" stop: {self.stop}" if self.stop is not None else ""
        _t = f" take: {self.take}" if self.take is not None else ""
        _r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
        _c = f" ({self.comment})" if self.comment else ""

        return f"[{_d}] POST-WARMUP-INIT ::{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"


class AssetType(StrEnum):
    CRYPTO = "CRYPTO"
    STOCK = "STOCK"
    FX = "FX"
    INDEX = "INDEX"


class MarketType(StrEnum):
    SPOT = "SPOT"
    MARGIN = "MARGIN"
    SWAP = "SWAP"
    FUTURE = "FUTURE"
    OPTION = "OPTION"


@dataclass(order=True)
class Instrument:
    """
    Instrument class.

     - 2025-06-11: Important change for FUTURE type: now instrument's symbol contains delivery date in format YYYYMMDD.
        So now for let's say september's BTCUSDT future, symbol would be BTCUSD.20250914
        and full id is `BINANCE.UM:FUTURE:BTCUSD.20250914`
    """

    symbol: str
    asset_type: AssetType
    market_type: MarketType
    exchange: str
    base: str
    quote: str
    settle: str
    exchange_symbol: str  # symbol used by the exchange
    tick_size: float  # minimal price step
    lot_size: float  # minimal position size
    min_size: float  # minimal allowed position size
    min_notional: float = 0.0  # minimal notional value
    initial_margin: float = 0.0  # initial margin
    maint_margin: float = 0.0  # maintenance margin
    liquidation_fee: float = 0.0  # liquidation fee
    contract_size: float = 1.0  # contract size
    onboard_date: datetime | None = None  # date when instrument was listed on the exchange
    delivery_date: datetime | None = None  # date when instrument is delivered
    delist_date: datetime | None = None  # date when instrument is delisted
    inverse: bool = False  # if true, then the future is inverse

    def __post_init__(self):
        # define how ordering works
        object.__setattr__(self, "sort_index", f"{self.exchange}:{self.market_type}:{self.symbol}")

    @property
    def price_precision(self):
        if not hasattr(self, "_price_precision"):
            self._price_precision = int(abs(np.log10(self.tick_size)))
        return self._price_precision

    @property
    def size_precision(self):
        if not hasattr(self, "_size_precision"):
            self._size_precision = int(abs(np.log10(self.lot_size)))
        return self._size_precision

    @property
    def asset(self) -> str:
        if self.base.startswith("1000"):
            return self.base.replace("1000", "")
        elif self.base.startswith("1000000"):
            return self.base.replace("1000000", "")
        else:
            return self.base

    def is_futures(self) -> bool:
        return self.market_type in [MarketType.FUTURE, MarketType.SWAP]

    def is_spot(self) -> bool:
        # TODO: handle margin better
        return self.market_type in [MarketType.SPOT, MarketType.MARGIN]

    def round_size_down(self, size: float) -> float:
        """
        Round down size to specified precision

        i.size_precision == 3
        i.round_size_up(0.1234) -> 0.123
        """
        return prec_floor(size, self.size_precision)

    def round_size_up(self, size: float) -> float:
        """
        Round up size to specified precision

        i.size_precision == 3
        i.round_size_up(0.1234) -> 0.124
        """
        return prec_ceil(size, self.size_precision)

    def round_price_down(self, price: float) -> float:
        """
        Round down price to specified precision

        i.price_precision == 3
        i.round_price_down(1.234999, 3) -> 1.234
        """
        return prec_floor(price, self.price_precision)

    def round_price_up(self, price: float) -> float:
        """
        Round up price to specified precision

        i.price_precision == 3
        i.round_price_up(1.234999) -> 1.235
        """
        return prec_ceil(price, self.price_precision)

    def service_signal(
        self,
        time: dt_64 | str | ITimeProvider,
        signal: float,
        price: float | None = None,
        stop: float | None = None,
        take: float | None = None,
        group: str = "",
        comment: str = "",
        options: dict[str, Any] | None = None,
        **kwargs,
    ) -> Signal:
        """
        Create service signal for the instrument
        """
        return self.signal(time, signal, price, stop, take, group, comment, options, is_service=True, **kwargs)

    def signal(
        self,
        time: dt_64 | str | ITimeProvider,
        signal: float,
        price: float | None = None,
        stop: float | None = None,
        take: float | None = None,
        group: str = "",
        comment: str = "",
        options: dict[str, Any] | None = None,
        is_service: bool = False,
        **kwargs,
    ) -> Signal:
        """
        Create signal for the instrument
        """
        return Signal(
            time=time.time() if isinstance(time, ITimeProvider) else time,
            instrument=self,
            signal=signal,
            price=price,
            stop=stop,
            take=take,
            group=group,
            comment=comment,
            options=(options or {}) | kwargs,
            is_service=is_service,
        )

    def target(
        self,
        time: dt_64 | str | ITimeProvider,
        amount: float,
        entry_price: float | None = None,
        stop_price: float | None = None,
        take_price: float | None = None,
        options: dict[str, Any] | None = None,
        **kwargs,
    ) -> TargetPosition:
        """
        Create target position for the instrument
        """
        return TargetPosition(
            time=time.time() if isinstance(time, ITimeProvider) else time,
            instrument=self,
            target_position_size=self.round_size_down(amount),
            entry_price=entry_price,
            stop_price=stop_price,
            take_price=take_price,
            options=(options or {}) | kwargs,
        )

    def get_amount_for_leverage(self, ctx: "IStrategyContext", leverage: float) -> float:
        q = ctx.quote(self)
        capital = ctx.get_total_capital()
        if q is None or not capital:
            return 0
        amount = (capital * leverage) / q.mid_price()
        return self.round_size_down(amount)

    def __hash__(self) -> int:
        return hash((self.symbol, self.exchange, self.market_type))

    def __eq__(self, other: Any) -> bool:
        if other is None or not isinstance(other, Instrument):
            return False
        return str(self) == str(other)

    def __str__(self) -> str:
        return ":".join([self.exchange, self.market_type, self.symbol])

    def __repr__(self) -> str:
        return self.__str__()

    def info(self):
        info_str = f"""
┌─────────────────────────────┐
│ Instrument Information      │
└─────────────────────────────┘
  Exchange:          {self.exchange}
  Symbol:            {self.symbol}
  Market Type:       {self.market_type}
  Base:              {self.base}
  Quote:             {self.quote}
  Exchange Symbol:   {self.exchange_symbol.upper()}
  Tick Size:         {self.tick_size}
  Lot Size:          {self.lot_size}
  Min Size:          {self.min_size}
  Min Notional:      {self.min_notional}
  Initial Margin:    {self.initial_margin}
  Maint. Margin:     {self.maint_margin}
  Onboard Date:      {self.onboard_date}
  Delist Date:       {self.delist_date}
"""
        print(info_str)


class TransactionCostsCalculator:
    """
    A class for calculating transaction costs for a trading strategy.
    Attributes
    ----------
    name : str
        The name of the transaction costs calculator.
    maker : float
        The maker fee, as a percentage of the transaction value.
    taker : float
        The taker fee, as a percentage of the transaction value.

    """

    name: str
    maker: float
    taker: float

    def __init__(self, name: str, maker: float, taker: float):
        self.name = name
        self.maker = maker / 100.0
        self.taker = taker / 100.0

    def get_execution_fees(
        self, instrument: Instrument, exec_price: float, amount: float, crossed_market=False, conversion_rate=1.0
    ):
        if crossed_market:
            return abs(amount * exec_price) * self.taker / conversion_rate
        else:
            return abs(amount * exec_price) * self.maker / conversion_rate

    def get_overnight_fees(self, instrument: Instrument, amount: float):
        return 0.0

    def get_funding_rates_fees(self, instrument: Instrument, amount: float):
        return 0.0

    def get_maker_fee_rate(self) -> float:
        """Get maker fee rate as decimal (e.g., 0.0002 for 0.02%)."""
        return self.maker

    def get_taker_fee_rate(self) -> float:
        """Get taker fee rate as decimal (e.g., 0.0004 for 0.04%)."""
        return self.taker

    def __repr__(self):
        return f"<{self.name}: {self.maker * 100:.4f} / {self.taker * 100:.4f}>"


ZERO_COSTS = TransactionCostsCalculator("Zero", 0.0, 0.0)


@dataclass
class TriggerEvent:
    """
    Event data for strategy trigger
    """

    time: dt_64
    type: str
    instrument: Optional[Instrument]
    data: Optional[Any]


@dataclass
class MarketEvent:
    """
    Market data update.
    """

    time: dt_64
    type: str
    instrument: Instrument | None
    data: Any
    is_trigger: bool = False

    def to_trigger(self) -> TriggerEvent:
        return TriggerEvent(self.time, self.type, self.instrument, self.data)

    def __repr__(self):
        _items = [
            f"time={self.time}",
            f"type={self.type}",
        ]
        if self.instrument is not None:
            _items.append(f"instrument={self.instrument}")
        _items.append(f"data={self.data}")
        return f"MarketEvent({', '.join(_items)})"


@dataclass
class Deal:
    id: str  # trade id
    order_id: str  # order's id
    time: dt_64  # time of trade
    amount: float  # signed traded amount: positive for buy and negative for selling
    price: float
    aggressive: bool
    fee_amount: float | None = None
    fee_currency: str | None = None


OrderType = Literal["MARKET", "LIMIT", "STOP_MARKET", "STOP_LIMIT"]
OrderSide = Literal["BUY", "SELL"]
OrderStatus = Literal["OPEN", "CLOSED", "CANCELED", "NEW", "PENDING"]


@dataclass
class OrderRequest:
    """
    Represents an order submission request (order intent).

    This is created by TradingManager and enriched by brokers with exchange-specific
    metadata in the options dict. The client_id is never mutated and is used for
    order tracking and health monitoring correlation.

    Attributes:
        instrument: The trading instrument
        quantity: Order quantity (positive for buy, negative for sell in some contexts)
        price: Limit price (None for market orders)
        order_type: "MARKET" or "LIMIT"
        side: "BUY" or "SELL"
        client_id: Unique identifier
        time_in_force: Order duration ("gtc", "ioc", "fok", etc.)
        options: Exchange-specific metadata (e.g., lighter_client_order_index)
    """

    instrument: Instrument
    quantity: float
    price: float | None = None
    order_type: OrderType = "LIMIT"
    side: OrderSide = "BUY"
    time_in_force: str = "gtc"
    client_id: str | None = None
    options: dict[str, Any] = field(default_factory=dict)


@dataclass
class Order:
    id: str
    type: OrderType
    instrument: Instrument
    time: dt_64
    quantity: float
    price: float
    side: OrderSide
    status: OrderStatus
    time_in_force: str
    client_id: str | None = None
    cost: float = 0.0
    options: dict[str, Any] = field(default_factory=dict)

    def __str__(self) -> str:
        return f"[{self.id}] {self.type} {self.side} {self.quantity} of {self.instrument} {('@ ' + str(self.price)) if self.price > 0 else ''} ({self.time_in_force}) [{self.status}]"


@dataclass
class AssetBalance:
    exchange: str
    currency: str
    free: float = 0.0
    locked: float = 0.0
    total: float = 0.0

    def __str__(self) -> str:
        return f"{self.exchange}:{self.currency} free={self.free:.2f} locked={self.locked:.2f} total={self.total:.2f}"

    def lock(self, lock_amount: float) -> None:
        self.locked += lock_amount
        self.free = self.total - self.locked

    def __add__(self, amount: float) -> "AssetBalance":
        self.total += amount
        self.free += amount
        return self

    def __sub__(self, amount: float) -> "AssetBalance":
        self.total -= amount
        self.free -= amount
        return self


DEFAULT_MAINTENANCE_MARGIN = 0.05


class Position:
    instrument: Instrument  # instrument for this position
    quantity: float = 0.0  # quantity positive for long and negative for short
    pnl: float = 0.0  # total cumulative position PnL in portfolio basic funds currency
    r_pnl: float = 0.0  # realized cumulative position PnL in portfolio basic funds currency
    market_value: float = 0.0  # position's market value in quote currency
    market_value_funds: float = 0.0  # position market value in portfolio funded currency
    position_avg_price: float = 0.0  # average position price
    position_avg_price_funds: float = 0.0  # average position price
    commissions: float = 0.0  # cumulative commissions paid for this position

    last_update_time: int = np.nan  # when price updated or position changed    # type: ignore
    last_update_price: float = np.nan  # last update price (actually instrument's price) in quoted currency
    last_update_conversion_rate: float = np.nan  # last update conversion rate

    # margin requirements
    maint_margin: float = 0.0
    _maint_margin_external: bool = False  # If True, maint_margin is managed by exchange (skip recalculation)

    # funding payment tracking
    cumulative_funding: float = 0.0  # cumulative funding paid (negative) or received (positive)
    funding_payments: list[FundingPayment]  # history of funding payments
    last_funding_time: dt_64 = np.datetime64("NaT")  # last funding payment time

    # - helpers for position processing
    _qty_multiplier: float = 1.0
    __pos_incr_qty: float = 0

    def __init__(
        self,
        instrument: Instrument,
        quantity: float = 0.0,
        pos_average_price: float = 0.0,
        r_pnl: float = 0.0,
        cumulative_funding: float = 0.0,
        commissions: float = 0.0,
    ) -> None:
        self.instrument = instrument
        self.funding_payments = []  # Initialize funding payments list

        self.reset()

        self.r_pnl = r_pnl
        self.cumulative_funding = cumulative_funding
        self.commissions = commissions

        if quantity != 0.0 and pos_average_price > 0.0:
            self.quantity = quantity
            self.position_avg_price = pos_average_price
            self.__pos_incr_qty = abs(quantity)

    def reset(self) -> None:
        """
        Reset position to zero
        """
        self.quantity = 0.0
        self.pnl = 0.0
        self.r_pnl = 0.0
        self.market_value = 0.0
        self.market_value_funds = 0.0
        self.position_avg_price = 0.0
        self.position_avg_price_funds = 0.0
        self.commissions = 0.0
        self.last_update_time = np.nan  # type: ignore
        self.last_update_price = np.nan
        self.last_update_conversion_rate = np.nan
        self.maint_margin = 0.0
        self._maint_margin_external = False
        self.cumulative_funding = 0.0
        self.funding_payments = []
        self.last_funding_time = np.datetime64("NaT")  # type: ignore
        self.__pos_incr_qty = 0
        self._qty_multiplier = self.instrument.contract_size

    def reset_by_position(self, pos: "Position") -> None:
        self.quantity = pos.quantity
        self.pnl = pos.pnl
        self.r_pnl = pos.r_pnl
        self.market_value = pos.market_value
        self.market_value_funds = pos.market_value_funds
        self.position_avg_price = pos.position_avg_price
        self.position_avg_price_funds = pos.position_avg_price_funds
        self.commissions = pos.commissions
        self.last_update_time = pos.last_update_time
        self.last_update_price = pos.last_update_price
        self.last_update_conversion_rate = pos.last_update_conversion_rate
        self.maint_margin = pos.maint_margin
        self._maint_margin_external = pos._maint_margin_external
        self.cumulative_funding = pos.cumulative_funding
        self.funding_payments = pos.funding_payments.copy() if hasattr(pos, "funding_payments") else []
        self.last_funding_time = pos.last_funding_time if hasattr(pos, "last_funding_time") else np.datetime64("NaT")
        self.__pos_incr_qty = pos.__pos_incr_qty

    @property
    def notional_value(self) -> float:
        return self.quantity * self.last_update_price / self.last_update_conversion_rate

    def _price(self, update: Quote | Trade) -> float:
        if isinstance(update, Quote):
            return update.bid if np.sign(self.quantity) > 0 else update.ask
        elif isinstance(update, Trade):
            return update.price
        raise ValueError(f"Unknown update type: {type(update)}")

    def change_position_by(
        self, timestamp: dt_64, amount: float, exec_price: float, fee_amount: float = 0, conversion_rate: float = 1
    ) -> tuple[float, float]:
        return self.update_position(
            timestamp,
            self.instrument.round_size_down(self.quantity + amount),
            exec_price,
            fee_amount,
            conversion_rate=conversion_rate,
        )

    def update_position(
        self, timestamp: dt_64, position: float, exec_price: float, fee_amount: float = 0, conversion_rate: float = 1
    ) -> tuple[float, float]:
        # - realized PnL of this fill
        deal_pnl = 0
        quantity = self.quantity
        comms = 0

        if quantity != position:
            pos_change = position - quantity
            direction = np.sign(pos_change)
            prev_direction = np.sign(quantity)

            # how many shares are closed/open
            qty_closing = min(abs(self.quantity), abs(pos_change)) * direction if prev_direction != direction else 0
            qty_opening = pos_change if prev_direction == direction else pos_change - qty_closing

            # - extract realized part of PnL
            if not np.isclose(qty_closing, 0.0):
                _abs_qty_close = abs(qty_closing)
                deal_pnl = qty_closing * (self.position_avg_price - exec_price)

                quantity += qty_closing
                self.__pos_incr_qty -= _abs_qty_close

                # - reset average price to 0 if smaller than minimal price change to avoid cumulative error
                if abs(quantity) < self.instrument.lot_size:
                    quantity = 0.0
                    self.position_avg_price = 0.0
                    self.__pos_incr_qty = 0

            # - if it has something to add to position let's update price and cost
            if not np.isclose(qty_opening, 0.0):
                _abs_qty_open = abs(qty_opening)

                pos_avg_price_raw = (_abs_qty_open * exec_price + self.__pos_incr_qty * self.position_avg_price) / (
                    self.__pos_incr_qty + _abs_qty_open
                )

                # - round position average price to be in line with how it's calculated by broker
                self.position_avg_price = self.instrument.round_price_down(pos_avg_price_raw)
                self.__pos_incr_qty += _abs_qty_open

            # - update position and position's price
            self.position_avg_price_funds = self.position_avg_price / conversion_rate
            self.quantity = position

            # - convert PnL to fund currency
            self.r_pnl += deal_pnl / conversion_rate

            # - update pnl
            self.update_market_price(time_as_nsec(timestamp), exec_price, conversion_rate)

            # - calculate transaction costs
            comms = fee_amount / conversion_rate
            self.commissions += comms

        return deal_pnl, comms

    def update_market_price_by_tick(self, tick: Quote | Trade, conversion_rate: float = 1) -> float:
        return self.update_market_price(tick.time, self._price(tick), conversion_rate)

    def update_position_by_deal(self, deal: Deal, conversion_rate: float = 1) -> tuple[float, float]:
        time = deal.time.as_unit("ns").asm8 if isinstance(deal.time, pd.Timestamp) else deal.time
        return self.change_position_by(
            timestamp=time,
            amount=deal.amount,
            exec_price=deal.price,
            fee_amount=deal.fee_amount or 0,
            conversion_rate=conversion_rate,
        )
        # - deal contains cumulative amount
        # return self.update_position(time, deal.amount, deal.price, deal.aggressive, conversion_rate)

    def update_market_price(self, timestamp: dt_64, price: float, conversion_rate: float) -> float:
        self.last_update_time = timestamp  # type: ignore
        self.last_update_price = price
        self.last_update_conversion_rate = conversion_rate

        if not np.isnan(price):
            u_pnl = self.unrealized_pnl()
            self.pnl = u_pnl + self.r_pnl + self.cumulative_funding
            if self.instrument.is_futures():
                # for derivatives market value of the position is the current unrealized PnL
                self.market_value = u_pnl
            else:
                # for spot: market value is the current value of the position
                # TODO: implement market value calculation for margin
                self.market_value = self.quantity * self.last_update_price * self._qty_multiplier

            # calculate mkt value in funded currency
            self.market_value_funds = self.market_value / conversion_rate

            # - update margin requirements
            self._update_maint_margin()

        return self.pnl

    def unrealized_pnl(self) -> float:
        if not np.isnan(self.last_update_price):
            return self.quantity * (self.last_update_price - self.position_avg_price) / self.last_update_conversion_rate  # type: ignore
        return 0.0

    def apply_funding_payment(self, funding_payment: FundingPayment, mark_price: float) -> float:
        """
        Apply a funding payment to this position.

        For perpetual swaps:
        - Positive funding rate: longs pay shorts
        - Negative funding rate: shorts pay longs

        Args:
            funding_payment: The funding payment event
            mark_price: The mark price at the time of funding

        Returns:
            The funding amount (negative if paying, positive if receiving)
        """
        if abs(self.quantity) < self.instrument.min_size:
            return 0.0

        # Calculate funding amount
        # Funding = Position Size * Mark Price * Funding Rate
        funding_amount = self.quantity * mark_price * funding_payment.funding_rate

        # For long positions with positive funding rate, amount is negative (paying)
        # For short positions with positive funding rate, amount is positive (receiving)
        funding_amount = -funding_amount

        # Update position state
        self.cumulative_funding += funding_amount
        self.r_pnl += funding_amount  # Funding affects realized PnL
        self.pnl += funding_amount  # And total PnL

        # Track funding payment history (limit to last 100)
        self.funding_payments.append(funding_payment)
        if len(self.funding_payments) > 100:
            self.funding_payments = self.funding_payments[-100:]

        self.last_funding_time = funding_payment.time

        return funding_amount

    def get_realized_price_pnl(self) -> float:
        """
        Get the realized price PnL for this position excluding funding.
        """
        return self.r_pnl - self.cumulative_funding

    def get_total_price_pnl(self) -> float:
        """
        Get the price PnL for this position excluding funding.
        """
        return self.pnl - self.cumulative_funding

    def is_open(self) -> bool:
        return abs(self.quantity) >= self.instrument.lot_size

    def get_amount_released_funds_after_closing(self, to_remain: float = 0.0) -> float:
        """
        Estimate how much funds would be released if part of position closed
        """
        d = np.sign(self.quantity)
        funds_release = self.market_value_funds
        if to_remain != 0 and self.quantity != 0 and np.sign(to_remain) == d:
            qty_to_release = max(self.quantity - to_remain, 0) if d > 0 else min(self.quantity - to_remain, 0)
            funds_release = qty_to_release * self.last_update_price / self.last_update_conversion_rate
        return abs(funds_release)

    @staticmethod
    def _t2s(t) -> str:
        return (
            np.datetime64(t, "ns").astype("datetime64[ms]").item().strftime("%Y-%m-%d %H:%M:%S")
            if not np.isnan(t)
            else "???"
        )

    def __str__(self):
        return " ".join(
            [
                f"{self._t2s(self.last_update_time)}",
                f"[{self.instrument}]",
                f"qty={self.quantity:.{self.instrument.size_precision}f}",
                f"entryPrice={self.position_avg_price:.{self.instrument.price_precision}f}",
                f"price={self.last_update_price:.{self.instrument.price_precision}f}",
                f"PNL: (unrealized={self.unrealized_pnl():.2f}",
                f"realized={self.r_pnl:.2f}",
                f"pnl={self.pnl:.2f})",
                f"value={self.market_value_funds:.2f}",
            ]
        )

    def __repr__(self):
        return self.__str__()

    def set_external_maint_margin(self, value: float) -> None:
        """
        Set maintenance margin from external source (exchange API).

        When set externally, the margin value won't be recalculated on price updates.
        This is used for live trading where exchanges provide accurate tiered margin values.

        Args:
            value: Maintenance margin value from exchange
        """
        self.maint_margin = value
        self._maint_margin_external = True

    def _update_maint_margin(self) -> None:
        # Skip recalculation if margin is managed externally (live trading with exchange-provided values)
        if self._maint_margin_external:
            return

        # Only apply maintenance margin for leveraged instruments (futures/swaps)
        # Spot positions don't have margin requirements since you own the actual asset
        if self.instrument.is_futures():
            # TODO: could be needed to multiply by qty_multiplier (contract multiplier)
            # but it needs to be correct and I think for crypto futures it's always 1
            maint_margin = self.instrument.maint_margin or DEFAULT_MAINTENANCE_MARGIN
            self.maint_margin = maint_margin * abs(self.quantity) * self.last_update_price
        else:
            self.maint_margin = 0.0


class CtrlChannel:
    """
    Controlled data communication channel
    """

    control: Event
    _queue: Queue  # we need something like disruptor here (Queue is temporary)
    name: str
    lock: Lock

    def __init__(self, name: str, sentinel=(None, None, None, None)):
        self.name = name
        self.control = Event()
        self.lock = Lock()
        self._sent = sentinel
        self._queue = Queue()
        self.start()

    def register(self, callback):
        pass

    def stop(self):
        if self.control.is_set():
            self.control.clear()
            self._queue.put(self._sent)  # send sentinel

    def start(self):
        self.control.set()

    def send(self, data):
        if self.control.is_set():
            self._queue.put(data)

    def receive(self, timeout: int | None = None) -> Any:
        try:
            return self._queue.get(timeout=timeout)
        except Empty:
            raise QueueTimeout(f"Timeout waiting for data on {self.name} channel")


class DataType(StrEnum):
    """
    Data type constants. Used for specifying the type of data and can be used for subscription to.
    Special value `DataType.ALL` can be used to subscribe to all available data types
    that are currently in use by the broker for other instruments.
    """

    ALL = "__all__"
    NONE = "__none__"
    QUOTE = "quote"
    TRADE = "trade"
    OHLC = "ohlc"
    ORDERBOOK = "orderbook"
    LIQUIDATION = "liquidation"
    AGGREGATED_LIQUIDATIONS = "aggregated_liquidations"
    FUNDING_RATE = "funding_rate"
    FUNDING_PAYMENT = "funding_payment"
    OPEN_INTEREST = "open_interest"
    OHLC_QUOTES = "ohlc_quotes"  # when we want to emulate quotes from OHLC data
    OHLC_TRADES = "ohlc_trades"  # when we want to emulate trades from OHLC data
    RECORD = "record"  # arbitrary timestamped data (actually liquidation and funding rates fall into this type)
    FUNDAMENTAL = "fundamental"  # fundamental data (with parameters)

    def __repr__(self) -> str:
        return self.value

    def __str__(self) -> str:
        return self.value

    def __eq__(self, other: Any) -> bool:
        if isinstance(other, DataType):
            return self.value == other.value
        return self.value == DataType.from_str(other)[0].value

    def __hash__(self) -> int:
        return hash(self.value)

    def __getitem__(self, *args, **kwargs) -> str:
        match self:
            case DataType.OHLC | DataType.OHLC_QUOTES:
                tf = args[0] if args else kwargs.get("timeframe")
                if not tf:
                    raise ValueError("Timeframe is not provided for OHLC subscription")
                return f"{self.value}({tf})"

            case DataType.AGGREGATED_LIQUIDATIONS:
                tf = args[0] if args else kwargs.get("timeframe")
                if not tf:
                    raise ValueError("Timeframe is not provided for AGGREGATED_LIQUIDATIONS")
                return f"{self.value}({tf})"

            case DataType.ORDERBOOK:
                # Check if args is a tuple containing another tuple (the nested case)
                if len(args) == 1 and isinstance(args[0], tuple):
                    # Unpack the nested tuple
                    inner_args = args[0]
                    if len(inner_args) == 2:
                        tick_size_pct, depth = inner_args
                    else:
                        raise ValueError(f"Invalid arguments for ORDERBOOK subscription: {inner_args}")
                elif len(args) == 2:
                    tick_size_pct, depth = args
                elif len(args) > 0:
                    raise ValueError(f"Invalid arguments for ORDERBOOK subscription: {args}")
                else:
                    tick_size_pct = kwargs.get("tick_size_pct", 0.01)
                    depth = kwargs.get("depth", 200)
                return f"{self.value}({tick_size_pct}, {depth})"

            case DataType.FUNDING_RATE:
                if len(args) == 0:
                    return f"{self.value}"
                elif len(args) == 1:
                    inner_args = args[0]
                    if len(inner_args) == 1:
                        return f"{self.value}({inner_args[0]})"
                    elif len(inner_args) == 2:
                        return f"{self.value}({inner_args[0]}, {inner_args[1]})"
                    else:
                        raise ValueError(f"Invalid arguments for FUNDING_RATE subscription: {inner_args}")
                else:
                    raise ValueError(f"Invalid arguments for FUNDING_RATE subscription: {args}")

            case DataType.FUNDAMENTAL:
                if len(args) == 0:
                    return f"{self.value}"
                else:
                    inner_args = args[0]
                    return (
                        f"{self.value}({', '.join(map(str, *args))})"
                        if isinstance(inner_args, tuple)
                        else f"{self.value}({inner_args})"
                    )

            case _:
                return self.value

    @staticmethod
    def from_str(value: Union[str, "DataType"]) -> tuple["DataType", dict[str, Any]]:
        """
        Parse subscription type from string.
        Returns: (subtype, params)

        Example:
        >>> Subtype.from_str("ohlc(1Min)")
        (Subtype.OHLC, {"timeframe": "1Min"})

        >>> Subtype.from_str("orderbook(0.01, 100)")
        (Subtype.ORDERBOOK, {"tick_size_pct": 0.01, "depth": 100})

        >>> Subtype.from_str("quote")
        (Subtype.QUOTE, {})

        >>> Subtype.from_str("funding_rate(all)")
        (Subtype.FUNDING_RATE, {"__all__": True})
        """
        if isinstance(value, DataType):
            return value, {}
        try:
            _value = value.lower()
            _has_params = DataType._str_has_params(value)
            if not _has_params and value.upper() not in DataType.__members__:
                return DataType.NONE, {}
            elif not _has_params:
                return DataType(_value), {}
            else:
                type_name, params_str = value.split("(", 1)
                params = [p.strip() for p in params_str.rstrip(")").split(",")]
                match type_name.lower():
                    case DataType.OHLC.value:
                        return DataType.OHLC, {"timeframe": time_delta_to_str(pd.Timedelta(params[0]).asm8.item())}

                    case DataType.AGGREGATED_LIQUIDATIONS.value:
                        return DataType.AGGREGATED_LIQUIDATIONS, {
                            "timeframe": time_delta_to_str(pd.Timedelta(params[0]).asm8.item())
                        }

                    case DataType.OHLC_QUOTES.value:
                        return DataType.OHLC_QUOTES, {
                            "timeframe": time_delta_to_str(pd.Timedelta(params[0]).asm8.item())
                        }

                    case DataType.OHLC_TRADES.value:
                        return DataType.OHLC_TRADES, {
                            "timeframe": time_delta_to_str(pd.Timedelta(params[0]).asm8.item())
                        }

                    case DataType.ORDERBOOK.value:
                        return DataType.ORDERBOOK, {"tick_size_pct": float(params[0]), "depth": int(params[1])}

                    case DataType.FUNDING_RATE.value:
                        if len(params) == 1 and params[0] == "all":
                            return DataType.FUNDING_RATE, {"__all__": True}
                        elif len(params) == 2 and params[0] == "all":
                            return DataType.FUNDING_RATE, {"__all__": True, "poll_interval_minutes": int(params[1])}
                        else:
                            raise ValueError(f"Invalid arguments for FUNDING_RATE subscription: {params}")

                    case DataType.FUNDAMENTAL.value:
                        if len(params) > 0:
                            return DataType.FUNDAMENTAL, {"fields": params}
                        return DataType.FUNDAMENTAL, {}

                    case _:
                        return DataType.NONE, {}
        except IndexError:
            raise ValueError(f"Invalid subscription type: {value}")

    @staticmethod
    def _str_has_params(value: str) -> bool:
        return "(" in value


class LiveTimeProvider(ITimeProvider):
    def __init__(self):
        self._start_ntp_thread()

    def time(self) -> dt_64:
        return time_now()

    def _start_ntp_thread(self):
        start_ntp_thread()


@dataclass
class RestoredState:
    """
    Container for state information needed to restart a strategy.

    This includes the current time, signals by instrument, and positions.
    """

    time: np.datetime64
    balances: list[AssetBalance]
    instrument_to_signal_positions: dict[Instrument, list[Signal]]
    instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
    positions: dict[Instrument, Position]

    def filter_by_exchange(self, exchange: str) -> "RestoredState":
        # TODO: maybe this needs to be mapped for BINANCE.PM, not sure
        return RestoredState(
            time=self.time,
            balances=[balance for balance in self.balances if balance.exchange == exchange],
            instrument_to_signal_positions={
                instrument: signals
                for instrument, signals in self.instrument_to_signal_positions.items()
                if instrument.exchange == exchange
            },
            instrument_to_target_positions={
                instrument: targets
                for instrument, targets in self.instrument_to_target_positions.items()
                if instrument.exchange == exchange
            },
            positions={
                instrument: position
                for instrument, position in self.positions.items()
                if instrument.exchange == exchange
            },
        )


class InstrumentsLookup:
    def get_lookup(self) -> dict[str, Instrument]: ...

    def find(
        self,
        exchange: str,
        base: str,
        quote: str | None = None,
        settle: str | None = None,
        market_type: MarketType | None = None,
    ) -> Instrument | None:
        for i in self.get_lookup().values():
            if (
                i.exchange == exchange
                and (
                    (
                        quote is not None
                        and ((i.base == base and i.quote == quote) or (i.base == quote and i.quote == base))
                    )
                    or (quote is None and i.base == base)
                )
                and (market_type is None or i.market_type == market_type)
            ):
                if settle is not None and i.settle is not None:
                    if i.settle == settle:
                        return i
                else:
                    return i
        return None

    def find_symbol(self, exchange: str, symbol: str, market_type: MarketType | None = None) -> Instrument | None:
        for i in self.get_lookup().values():
            if (
                (i.exchange == exchange)
                and (i.symbol == symbol)
                and (market_type is None or i.market_type == market_type)
            ):
                return i

        return None

    def find_instruments(
        self,
        exchange: str,
        base: str | None = None,
        quote: str | None = None,
        market_type: MarketType | None = None,
        as_of: str | pd.Timestamp | None = None,
    ) -> list[Instrument]:
        """
        Find instruments by exchange, quote, market type and as of date.
        If as_of is not None, then only instruments that are not delisted after as_of date will be returned.
        - exchange: str - exchange name
        - base: str | None - base currency
        - quote: str | None - quote currency
        - market_type: MarketType | None - market type
        - as_of is a string in format YYYY-MM-DD or pd.Timestamp or None
        """
        _limit_time = pd.Timestamp(as_of) if as_of else None
        return [
            i
            for i in self.get_lookup().values()
            if i.exchange == exchange
            and (
                base is None or (i.base == base or i.base == f"1000{base}")
            )  # this is a hack to support 1000DOGEUSDT and others
            and (quote is None or i.quote == quote)
            and (market_type is None or i.market_type == market_type)
            and (
                _limit_time is None
                or (i.onboard_date is None or pd.Timestamp(i.onboard_date).tz_localize(None) <= _limit_time)
            )
            and (
                _limit_time is None
                or (i.delist_date is None or pd.Timestamp(i.delist_date).tz_localize(None) >= _limit_time)
            )
        ]

    def find_aux_instrument_for(
        self, instrument: Instrument, base_currency: str, market_type: MarketType | None = None
    ) -> Instrument | None:
        """
        Tries to find aux instrument (for conversions to funded currency)
        for example:
            ETHBTC -> BTCUSDT for base_currency USDT
            EURGBP -> GBPUSD for base_currency USD
            ...
        """
        if market_type is None:
            market_type = instrument.market_type
        base_currency = base_currency.upper()
        if instrument.quote != base_currency:
            return self.find(instrument.exchange, instrument.quote, base_currency, market_type=market_type)
        return None

    def __getitem__(self, spath: str) -> list[Instrument]:
        """
        Helper method for finding instruments by pattern.
        It's convenient to use in research mode.
        """
        res = []
        c = re.compile(spath)
        for k, v in self.get_lookup().items():
            if re.match(c, k):
                res.append(v)
        return res


class FeesLookup:
    def find_fees(self, exchange: str, spec: str | None) -> TransactionCostsCalculator: ...
