import sys
import json
import re
import math
from traceback import print_tb

from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel, QPushButton,
                             QHBoxLayout, QScrollArea, QGroupBox, QGridLayout, QFrame,
                             QMessageBox, QDoubleSpinBox)
from PyQt5.QtGui import QFont, QColor
from PyQt5.QtCore import (Qt, QTimer, QThread, pyqtSignal)
import pandas as pd
import key
from pybit.unified_trading import HTTP
import time
import datetime
import pytz


# Initialize Bybit API session
session = HTTP(
    api_key=key.APIK,
    api_secret=key.APISK,
)


# Function to extract expiry date from contract name
def extract_expiry_date(contract_name):
    try:
        date = contract_name.split('-')[1]
        if date:
            return date
    except:
        return None


# Function to extract strike price from option contract name
def extract_strike_price(contract_name):
    pattern = r'BTC-\d+[A-Z]+\d+-(\d+)'
    match = re.search(pattern, contract_name)
    if match:
        return float(match.group(1))
    return None


# Function to determine if a contract is an option or a future
def is_option(contract_name):
    return '-P' in contract_name or '-C' in contract_name


# Function to determine option type (Call or Put)
def get_option_type(contract_name):
    if contract_name.endswith('-C'):
        return "Call"
    elif contract_name.endswith('-P'):
        return "Put"
    return None


# Function to calculate current DVG for futures
def round_down(value, decimals):
    factor = 10 ** decimals
    return math.floor(value * factor) / factor


def calculate_futures_dvg(futures_contract, spot_price):
    try:
        futures_price = float(futures_contract['price'])
        return round((futures_price - spot_price) * 100 / spot_price, 3)
    except Exception as e:
        print(f"Error calculating futures DVG: {e}")
        return None


def calculate_options_dvg(call_contract, put_contract, strike_price, spot_price):
    """Calculate DVG for options based on call and put prices"""
    try:
        # Extract prices
        if call_contract['side'] == 'Купить':
            call_sell = float(call_contract['price'])
            call_buy = None
        else:
            call_sell = None
            call_buy = float(call_contract['price'])

        if put_contract['side'] == 'Купить':
            put_sell = float(put_contract['price'])
            put_buy = None
        else:
            put_sell = None
            put_buy = float(put_contract['price'])

        # Calculate divergence based on pricing
        # This follows the logic in your original DVG calculation code
        if strike_price <= spot_price:
            if call_sell is not None and put_buy is not None:
                pbcs = round_down((call_sell - put_buy + strike_price - spot_price) * 100 / spot_price, 3)
                return pbcs
            elif put_sell is not None and call_buy is not None:
                pscb = round_down((put_sell - call_buy + strike_price - spot_price) * 100 / spot_price, 3)
                return pscb
        elif strike_price > spot_price:
            if call_sell is not None and put_buy is not None:
                pbcs = round_down((call_sell - put_buy - strike_price + spot_price) * 100 / spot_price, 3)
                return pbcs
            elif put_sell is not None and call_buy is not None:
                pscb = round_down((put_sell - call_buy - strike_price + spot_price) * 100 / spot_price, 3)
                return pscb

        return None
    except Exception as e:
        print(f"Error calculating option DVG: {e}")
        return None


class DVGWorker(QThread):
    dvg_updated = pyqtSignal(float)
    auto_close_by_expiry = pyqtSignal()

    def __init__(self, position_data):
        super().__init__()
        self.position_data = position_data
        self.running = True

    def run(self):
        while self.running:
            try:
                # print(self.position_data)
                if "BTCUSDT" in self.position_data:
                    symbol = "BTCUSDT"
                    spot_price = float(
                        session.get_tickers(category="linear", symbol=symbol).get('result', {}).get('list')[0].get(
                            'lastPrice'))
                if "BTCUSDC" in self.position_data:
                    symbol = "BTCUSDC"
                    spot_price = float(
                        session.get_tickers(category="spot", symbol=symbol).get('result', {}).get('list')[0].get(
                            'lastPrice'))
                if "BTCPERP" in self.position_data:
                    symbol = "BTCPERP"
                    spot_price = float(
                        session.get_tickers(category="linear", symbol=symbol).get('result', {}).get('list')[0].get(
                            'lastPrice'))
                if "ETHUSDT" in self.position_data:
                    symbol = "ETHUSDT"
                    spot_price = float(
                        session.get_tickers(category="linear", symbol=symbol).get('result', {}).get('list')[0].get(
                            'lastPrice'))
                if "ETHUSDC" in self.position_data:
                    symbol = "ETHUSDC"
                    spot_price = float(
                        session.get_tickers(category="spot", symbol=symbol).get('result', {}).get('list')[0].get(
                            'lastPrice'))
                if "ETHPERP" in self.position_data:
                    symbol = "ETHPERP"
                    spot_price = float(
                        session.get_tickers(category="linear", symbol=symbol).get('result', {}).get('list')[0].get(
                            'lastPrice'))


                contract_keys = [k for k in self.position_data.keys() if k != 'dvg']
                option_keys = [k for k in contract_keys if k.endswith('-P') or k.endswith('-C') or k.endswith('-USDT')]
                futures_keys = [k for k in contract_keys if ('BTC-' in k or 'ETH-' in k) and not (k.endswith('-P') or k.endswith('-C') or k.endswith('-USDT'))]

                new_dvg = None

                if len(option_keys) == 2:
                    call_key = next((k for k in option_keys if k.endswith('-C') or k.endswith('-C-USDT')), None)
                    put_key = next((k for k in option_keys if k.endswith('-P') or k.endswith('-P-USDT')), None)

                    if call_key and put_key:
                        strike_price = float(call_key.split('-')[2])
                        position_call = self.position_data[call_key]
                        position_put = self.position_data[put_key]

                        side_call = position_call.get('side', 'Продать')  # По умолчанию 'Продать'
                        quantity_call = position_call.get('quantity', 0.1)  # или любое другое значение по умолчанию

                        side_put = position_put.get('side', 'Продать')  # По умолчанию 'Продать'
                        quantity_put = position_put.get('quantity', 0.1)  # или любое другое значение по умолчанию

                        ticker_data_call = session.get_tickers(category="option", symbol=call_key).get('result', {}).get(
                            'list', [])

                        ticker_data_put = session.get_tickers(category="option", symbol=put_key).get('result',{}).get(
                            'list', [])

                        if ticker_data_call:
                            ticker_call = ticker_data_call[0]
                            ticker_put = ticker_data_put[0]

                            # Выбор цены в зависимости от стороны сделки
                            if side_call == 'Купить':
                                price_call = float(ticker_call.get('bid1Price', 0))
                            else:  # 'Продать'
                                price_call = float(ticker_call.get('ask1Price', 0))

                            if side_put == 'Купить':
                                price_put = float(ticker_put.get('bid1Price', 0))
                            else:  # 'Продать'
                                price_put = float(ticker_put.get('ask1Price', 0))

                            formatted_data_call = {
                                'category': 'option',
                                'orderType': 'Limit',
                                'side': side_call,
                                'price': price_call,
                                'quantity': quantity_call
                            }

                            formatted_data_put = {
                                'category': 'option',
                                'orderType': 'Limit',
                                'side': side_put,
                                'price': price_put,
                                'quantity': quantity_put
                            }

                            new_dvg = calculate_options_dvg(
                                formatted_data_call,
                                formatted_data_put,
                                strike_price,
                                spot_price
                            )
                        else:
                            new_dvg = calculate_options_dvg(
                                self.position_data[call_key],
                                self.position_data[put_key],
                                strike_price,
                                spot_price
                            )

                elif len(futures_keys) == 1:
                    futures_key = futures_keys[0]
                    ticker_data = session.get_tickers(category="linear", symbol=futures_key).get('result', {}).get(
                        'list', [])
                    futures_contract = self.position_data[futures_key]
                    side = futures_contract.get('side')
                    quantity = futures_contract.get('quantity')

                    if ticker_data:
                        ticker = ticker_data[0]

                        # Выбор цены в зависимости от стороны сделки
                        if side == 'Купить':
                            price = float(ticker.get('bid1Price', 0))
                        else:  # 'Продать'
                            price = float(ticker.get('ask1Price', 0))

                        formatted_data = {
                            'category': 'linear',
                            'orderType': 'Limit',
                            'side': side,
                            'price': price,
                            'quantity': quantity
                        }

                        new_dvg = calculate_futures_dvg(
                            formatted_data,
                            spot_price
                        )
                    else:
                        new_dvg = calculate_futures_dvg(
                            futures_contract,
                            spot_price
                        )

                if new_dvg is not None:
                    self.dvg_updated.emit(new_dvg)

                    # Авто-закрытие по времени экспирации
                    for key in self.position_data:
                        expiry_str = extract_expiry_date(key)
                        if expiry_str:
                            try:
                                expiry_date = datetime.datetime.strptime(expiry_str, "%d%b%y").date()
                                now_gmt5 = datetime.datetime.now(pytz.timezone("Asia/Yekaterinburg"))  # GMT+5

                                if now_gmt5.date() == expiry_date and now_gmt5.hour == 13:
                                    print(f"[AUTO-CLOSE] Triggered for {key} at {now_gmt5}")
                                    self.auto_close_by_expiry.emit()  # посылаем сигнал авто-закрытия
                                    return
                            except Exception as e:
                                print(f"Ошибка при разборе даты экспирации: {e}")



            except Exception as e:
                print(f"Error in DVGWorker: {e}")
            time.sleep(1)  # обновлять раз в секунду

    def stop(self):
        self.running = False


class PositionViewer(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Crypto Position Viewer")
        self.setGeometry(100, 100, 1000, 600)
        self.positions = []
        self.position_widgets = {}  # Словарь: позиция (id) -> виджет

        self.excel_file = 'data(2).xlsx'  # Путь к Excel файлу

        # Main layout
        main_layout = QVBoxLayout()

        # Title
        title_label = QLabel("Crypto Position Viewer")
        title_label.setFont(QFont("Arial", 16, QFont.Bold))
        title_label.setAlignment(Qt.AlignCenter)
        main_layout.addWidget(title_label)

        # Scroll area for positions
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        scroll_widget = QWidget()
        self.scroll_layout = QVBoxLayout(scroll_widget)

        try:
            # Загружаем DataFrame один раз, сохраняем в self.df
            self.df = pd.read_excel(self.excel_file)

            for idx, row in self.df.iterrows():
                try:
                    data = row.iloc[0]
                    position_data = json.loads(str(data).replace("'", '"'))
                    position_data['_excel_index'] = idx  # Запоминаем индекс

                    self.positions.append(position_data)

                    position_widget = self.create_position_widget(position_data)
                    self.scroll_layout.addWidget(position_widget)
                    self.position_widgets[idx] = position_widget

                except json.JSONDecodeError as e:
                    print(f"JSON decode error: {e}")
                except Exception as e:
                    print(f"Error processing position: {e}")

        except Exception as e:
            error_label = QLabel(f"Error loading positions: {e}")
            error_label.setStyleSheet("color: red;")
            self.scroll_layout.addWidget(error_label)

        scroll_area.setWidget(scroll_widget)
        main_layout.addWidget(scroll_area)

        # Status bar
        self.status_bar = QLabel("Total positions: " + str(len(self.positions)))
        self.status_bar.setAlignment(Qt.AlignRight)
        main_layout.addWidget(self.status_bar)

        self.setLayout(main_layout)

    def create_position_widget(self, position_data):
        # Create a group box for the position
        position_box = QGroupBox()
        position_box.setTitle("")

        # Create grid layout for position details
        grid_layout = QGridLayout()

        # Старый DVG (initial)
        old_dvg = position_data.get('dvg', 'N/A')
        dvg_label_old = QLabel(f"Исходный DVG: {old_dvg}")
        dvg_label_old.setFont(QFont("Arial", 10, QFont.Bold))
        grid_layout.addWidget(dvg_label_old, 0, 0, 1, 2)

        # Новый DVG
        dvg_label_new = QLabel("Текущий DVG: Обновляется...")
        dvg_label_new.setObjectName("dvg_label")
        dvg_label_new.setFont(QFont("Arial", 10, QFont.Bold))
        grid_layout.addWidget(dvg_label_new, 0, 2, 1, 2)

        # Порог автозакрытия
        threshold_label = QLabel("Порог авто-закрытия:")
        threshold_spinbox = QDoubleSpinBox()
        threshold_spinbox.setSuffix(" %")
        threshold_spinbox.setDecimals(3)
        threshold_spinbox.setRange(0.001, 1000.0)
        threshold_spinbox.setValue(1000.0)
        grid_layout.addWidget(threshold_label, 1, 0)
        grid_layout.addWidget(threshold_spinbox, 1, 1)

        # Заголовки таблицы
        headers = ["Contract", "Type", "Side", "Price", "Quantity", "Leverage"]
        for i, header in enumerate(headers):
            header_label = QLabel(header)
            header_label.setFont(QFont("Arial", 10, QFont.Bold))
            grid_layout.addWidget(header_label, 2, i)

        # Разделительная линия
        line = QFrame()
        line.setFrameShape(QFrame.HLine)
        line.setFrameShadow(QFrame.Sunken)
        grid_layout.addWidget(line, 3, 0, 1, len(headers))

        # Детали позиции
        row = 4
        for key, value in position_data.items():
            if key != 'dvg':
                contract_label = QLabel(key)
                grid_layout.addWidget(contract_label, row, 0)

                contract_type = "Option" if is_option(key) else "Futures"
                type_label = QLabel(contract_type)
                grid_layout.addWidget(type_label, row, 1)

                if isinstance(value, dict):
                    if value.get('side') == 'Купить':
                        side_label = QLabel('Продать')
                        side_label.setStyleSheet("color: red;")
                    elif value.get('side') == 'Продать':
                        side_label = QLabel('Купить')
                        side_label.setStyleSheet("color: green;")
                    grid_layout.addWidget(side_label, row, 2)

                    price_label = QLabel(str(value.get('price', 'N/A')))
                    grid_layout.addWidget(price_label, row, 3)

                    quantity_label = QLabel(str(value.get('quantity', 'N/A')))
                    grid_layout.addWidget(quantity_label, row, 4)

                    leverage_label = QLabel(str(value.get('leverage', 'N/A')))
                    grid_layout.addWidget(leverage_label, row, 5)

                row += 1

        # Кнопки
        button_layout = QHBoxLayout()
        close_button = QPushButton("Close Position")
        button_layout.addWidget(close_button)
        grid_layout.addLayout(button_layout, row, 0, 1, len(headers))

        # Обработчики
        close_button.clicked.connect(lambda: self.close_position(position_data))

        # Создаём и запускаем поток
        dvg_worker = DVGWorker(position_data)
        dvg_worker.dvg_updated.connect(lambda new_dvg: self.handle_dvg_update(new_dvg, position_data, position_box))
        dvg_worker.auto_close_by_expiry.connect(lambda: self.close_position_exp(position_data))
        dvg_worker.auto_close_by_expiry.connect(lambda: QMessageBox.information(self, "Auto-Close by Expiry",
                                                                                "Position auto-closed at expiry (13:00 GMT+5)."))
        dvg_worker.start()

        # Храним ссылку на поток, чтобы не удалился
        if not hasattr(self, 'dvg_threads'):
            self.dvg_threads = []
        self.dvg_threads.append(dvg_worker)

        position_box.setLayout(grid_layout)
        return position_box

    def handle_dvg_update(self, new_dvg, position_data, group_box):
        dvg_label = group_box.findChild(QLabel, "dvg_label")
        if dvg_label:
            dvg_label.setText(f"DVG: {new_dvg}")
            color = "green" if new_dvg >= 0 else "red"
            dvg_label.setStyleSheet(f"color: {color}; font-weight: bold;")

        threshold_spinbox = group_box.findChild(QDoubleSpinBox)
        if threshold_spinbox:
            threshold = threshold_spinbox.value()
            if abs(new_dvg) >= abs(threshold) and threshold != 0.0:
                self.close_position(position_data)
                QMessageBox.warning(self, "Auto-Close Triggered",
                                    f"Position auto-closed: DVG ({new_dvg}) exceeded threshold ({threshold})")

    def close_position(self, position_data):
        print(f"Closing position: {position_data}")

        idx_to_remove = position_data.get('_excel_index')
        if idx_to_remove is not None:
            try:
                # Удаляем позицию из DataFrame
                self.df = self.df.drop(idx_to_remove)
                self.df.to_excel(self.excel_file, index=False)

                # Удаляем виджет
                widget_to_remove = self.position_widgets.get(idx_to_remove)
                if widget_to_remove:
                    widget_to_remove.setParent(None)
                    widget_to_remove.deleteLater()
                    del self.position_widgets[idx_to_remove]

                # Удаляем из self.positions
                self.positions = [pos for pos in self.positions if pos.get('_excel_index') != idx_to_remove]

                # Останавливаем и удаляем соответствующий поток dvg_worker
                worker_to_remove = None
                for worker in self.dvg_threads:
                    if hasattr(worker, 'position_data') and worker.position_data.get('_excel_index') == idx_to_remove:
                        worker.stop()
                        worker.wait()
                        worker_to_remove = worker
                        break
                if worker_to_remove:
                    self.dvg_threads.remove(worker_to_remove)

                # Обновляем статус
                self.status_bar.setText("Total positions: " + str(len(self.positions)))

                QMessageBox.information(self, "Position Closed",
                                        "Position was successfully closed and removed from Excel.")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to remove position from Excel: {e}")
        else:
            QMessageBox.warning(self, "Warning", "Index to remove not found.")

    def close_position_exp(self, position_data):
        """Close the position - this would interact with the exchange API"""
        print(f"Closing position: {position_data}")
        # Implement the actual position closing logic here
        # This should create closing orders for each part of the position

    def closeEvent(self, event):
        if hasattr(self, 'dvg_threads'):
            for thread in self.dvg_threads:
                thread.stop()
                thread.wait()
        event.accept()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle('Fusion')  # Use Fusion style for a more modern look
    viewer = PositionViewer()
    viewer.show()
    sys.exit(app.exec_())
почему при авто закрытии позиции по дате экспирации dvg для этой позиции перестаёт считаться