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

    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 __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:
    instrument: Instrument
    quantity: float
    price: float | 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:
    free: float = 0.0
    locked: float = 0.0
    total: float = 0.0

    def __str__(self) -> str:
        return f"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

    # 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,
    ) -> None:
        self.instrument = instrument
        self.funding_payments = []  # Initialize funding payments list

        self.reset()
        if quantity != 0.0 and pos_average_price > 0.0:
            self.quantity = quantity
            self.position_avg_price = pos_average_price
            self.r_pnl = r_pnl
            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.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.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
            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 total_pnl(self) -> float:
        # TODO: account for commissions
        return self.r_pnl + self.unrealized_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_funding_pnl(self) -> float:
        """Get cumulative funding PnL for this position."""
        return 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 _update_maint_margin(self) -> None:
        # 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: dict[str, AssetBalance]
    instrument_to_signal_positions: dict[Instrument, list[Signal]]
    instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
    positions: dict[Instrument, Position]


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