import sys
import json
import re
import math

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
import POST

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 == 16:
                                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.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 not in ['dvg', '_excel_index']:
            contract_label = QLabel(key)
            grid_layout.addWidget(contract_label, row, 0)

            type_label = QLabel(value.get('category', 'N/A'))
            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:
        old_dvg = position_data.get('dvg', 'N/A')
        threshold = threshold_spinbox.value()
        if abs(old_dvg - 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):
    psc = position_data.copy()
    del psc['dvg']
    del psc['_excel_index']

    post = POST.PostOrders()
    post.post_orders(psc)

    # 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):
    for key in position_data:
        if key not in ['dvg', '_excel_index']:
            if not extract_expiry_date(key):
                psc = position_data[key]
                post = POST.PostOrders()
                post.post_orders({key: psc})

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 поток останавливался так же как при закрытии при дате экспирации