# --- ÍNDICE DO ARQUIVO ---
#
# CLASSES:
#   TOption
#     . __init__ (Parâmetros: Nenhum | Retorno: None)
#     . CopyOptionTo (Parâmetros: target_option: 'TOption' | Retorno: None)
#     . BlackScholesSimple (Parâmetros: S: float, dias_para_vencimento: int, r_anual: float, v_anual: float | Retorno: float)
#   TObjectList (Hereda de list)
#     . Last (Propriedade | Retorno: Any | Tipo: TOption ou None)
#     . Add (Parâmetros: item: Any | Retorno: None)
#     . Clear (Parâmetros: Nenhum | Retorno: None)
#     . Destroy (Parâmetros: Nenhum | Retorno: None)
#   TChart (Simulação)
#     . ClearChart (Parâmetros: Nenhum | Retorno: None)
#     . CriarLegenda (Parâmetros: *args | Retorno: None)
#     . criarSerie (Parâmetros: name, *args | Retorno: TLineSeries)
#     . View3D (Propriedade | Retorno: bool)
#   TLineSeries (Simulação)
#     . __init__ (Parâmetros: name | Retorno: None)
#     . AddXY (Parâmetros: x, y, label="", color="" | Retorno: None)
#     . Addarray (Parâmetros: x_values, y_values | Retorno: None)
#   TStringGrid (Simulação)
#     . __init__ (Parâmetros: Nenhum | Retorno: None)
#     . RowCount (Propriedade | Tipo: int)
#     . ColCount (Propriedade | Tipo: int)
#     . Cells (Parâmetros: col, row | Retorno: _GridCell)
#     . AutoSizeColumns (Parâmetros: Nenhum | Retorno: None)
#   _GridCell (Auxiliar Interna)
#     . __init__ (Parâmetros: cells_dict, col, row | Retorno: None)
#     . value (Propriedade | Tipo: str)
#     . __str__ (Parâmetros: Nenhum | Retorno: str)
#   TStringList (Hereda de list, Simulação)
#     . CommaText (Propriedade | Retorno: str)
#     . Add (Parâmetros: item | Retorno: None)
#   TTipoAnalise (Enum)
#   TTipoCalculoPayoff (Enum)
#   TAnaliseEsperancaData (Classe de Dados)

#   TEstrutura
#     . __init__ (Parâmetros: Nenhum | Retorno: None)
#     . Destroy (Parâmetros: Nenhum | Retorno: None)
#     . _GerarDistribuicaoDeResultados (Interno) (Parâmetros: mean: float, volatility: float, CDI: float, Tipo: TTipoAnalise, diasAntesDoPrimeiroVenc: int, AnaliseData: TAnaliseEsperancaData | Retorno: None)
#     . DecodificarPapelOpcao (Método de Classe) (Parâmetros: papel: str | Retorno: Tuple[str, str, float, str])
#     . ImportarEstruturaDeStringTransacoes (Método de Classe) (Parâmetros: dados_string: str | Retorno: 'TEstrutura')
#     . _ExecutarAnaliseLucroEsperado (Interno) (Parâmetros: Sigma: float, mean: float, N_dias_projecao: int, CDI: float, Tipo: TTipoAnalise, diasAntesDoPrimeiroVenc: int, Chart: Optional[TChart], PlotExpectedProfit: bool, PlotProfit: bool, PlotSigmas: bool, PlotFDP: bool, PlotAtivo: bool, limiteMin: float, limiteMax: float | Retorno: float)
#     . _CalcularEsperancaNaFaixa (Interno) (Parâmetros: AnaliseData: TAnaliseEsperancaData, limiteMin: float, limiteMax: float, step: float | Retorno: float)
#     . _PlotarDadosCalculoEsperanca (Interno) (Parâmetros: Chart: TChart, AnaliseData: TAnaliseEsperancaData, limiteMin: float, limiteMax: float, PlotFDP: bool, PlotProfit: bool, PlotExpectedProfit: bool | Retorno: None)
#     . _PrecificarOpcaoBS (Interno) (Parâmetros: Opcao: TOption, PrecoAtivo: float, diasParaVencimento: int, CDI: float | Retorno: float)
#     . PlotarPayoffNoVencimento (Parâmetros: Chart: TChart, minv: float, maxv: float, CDI: float, plotarStrikes: bool, plotarValorAtivo: bool, plotarSigmas: bool | Retorno: None)
#     . AddOption (Parâmetros: Quant: int, Tipo: str, Oper: str, strikeN: float, Vencimento: int | Retorno: None)
#     . AddOption_with_name_and_value (Parâmetros: Oper: str, Nome: str, Tipo: str, strike: float, Quant: int, ValorIn: float | Retorno: None)
#     . CalcPayoffnoVencimento (Parâmetros: PrecoAtivo: float, CDI: Optional[float] = None | Retorno: float)
#     . CalcPayoffAntecipado (Parâmetros: PrecoAtivo: float, diasAntesDoPrimeiroVenc: int, CDI: float | Retorno: float)
#     . CalcExpectedProfitNoVencimento (Parâmetros: Sigma: float, mean: float, CDI: float, Chart: Optional[TChart], PlotExpectedProfit: bool, PlotProfit: bool, PlotSigmas: bool, PlotFDP: bool, PlotAtivo: bool | Retorno: float)
#     . CalcExpectedProfitNoVencimentoComLimites (Parâmetros: Sigma: float, mean: float, CDI: float, Chart: Optional[TChart], PlotExpectedProfit: bool, PlotProfit: bool, PlotSigmas: bool, PlotFDP: bool, PlotAtivo: bool, limiteMin: float, limiteMax: float | Retorno: float)
#     . CalcExpectedProfitAntecipado (Parâmetros: Sigma: float, mean: float, diasAntesDoPrimeiroVenc: int, CDI: float, Chart: Optional[TChart], PlotExpectedProfit: bool, PlotProfit: bool, PlotSigmas: bool, PlotFDP: bool, PlotAtivo: bool, limiteMin: float, limiteMax: float | Retorno: float)
#     . CalcGraphLimits (Parâmetros: minv: float, maxv: float, N: float | Retorno: Tuple[float, float])
#     . CopyEstruturaTo (Parâmetros: EstruturaLcl: 'TEstrutura' | Retorno: None)
#     . BlackScholesSimple (Parâmetros: ValorAtivo: float, dias: int, ratePercAnual: float | Retorno: float)
#     . BlackScholesSerie (Parâmetros: S: float, r: float, v: float, Vencimentos: int | Retorno: float)
#     . GetMaiorVencimento (Parâmetros: Nenhum | Retorno: int)
#     . GetMenorVencimento (Parâmetros: Nenhum | Retorno: int)
#     . GetVencimentos (Parâmetros: Nenhum | Retorno: Tuple[int, int])
#     . GetXLimits (Parâmetros: minX_ref: float, maxX_ref: float | Retorno: Tuple[float, float])
#     . GetSymbolList (Parâmetros: Nenhum | Retorno: str)
#     . CalcExpectedProfitBSPonderado (Parâmetros: Sigma: float, mean: float, N: int, MaiorVencimento: int, menorVencimento: int, CDI: float, GanhoMin: float, Chart: Optional[TChart] | Retorno: float)
#     . PlotarEstruturaNow (Parâmetros: Chart: TChart, minv: float, maxv: float, CDI: float, plotarStrikes: bool | Retorno: None)
#     . PlotarEstruturaThen (Parâmetros: Chart: TChart, minv: float, maxv: float, CDI: float, plotarStrikes: bool | Retorno: None)
#     . CalcValorIn (Parâmetros: Nenhum | Retorno: float)
#     . FazerEstruturaAgressiva (Parâmetros: agressividade: float | Retorno: 'TEstrutura')
#     . FazerEstruturaAgressivaSpread (Parâmetros: agressividade: float | Retorno: 'TEstrutura')
#     . EstruturaToGrid (Parâmetros: Nenhum | Retorno: pd.DataFrame)
#     . EstruturaToGrid_Detalhes (Parâmetros: Grid: TStringGrid | Retorno: None)
#     . FixValorIn (Parâmetros: Nenhum | Retorno: None)
#     . NormalizarQuants (Parâmetros: Nenhum | Retorno: None)
#     . PayOffsToGridBS (Parâmetros: minv: float, maxv: float, CDI: float | Retorno: pd.DataFrame)
#     . Plotar_Strikes_ValorAtivo_Sigma (Parâmetros: Chart: TChart, plotarStrikes: bool, plotarValorAtivo: bool, plotarSigmas: bool | Retorno: None)
#     . SetarEstrutura (Parâmetros: tipos: List[str], quants: List[int] | Retorno: None)
#     . CotarEstrutura (Parâmetros: PreencherValorIn: bool, PreencherValorOut: bool | Retorno: None)
#     . DerivarEstruturaParaVencimentoFuturo (Parâmetros: VencimentoReferencia: int | Retorno: 'TEstrutura')
#     . DerivarEstruturaComVencimentoUnico (Parâmetros: Nenhum | Retorno: 'TEstrutura')
#     . CalcProbabilidadeNaFaixa (Parâmetros: Sigma: float, mean: float, N: int, limiteMin: float, limiteMax: float | Retorno: float)
#     . CalcExpectedProfitCondicionalDoisVencimentos (Parâmetros: Sigma: float, mean: float, N1: int, N2: int, CDI: float, Chart: Optional[TChart] | Retorno: float)
#     . CotarEstruturaAPI (Parâmetros: Nenhum | Retorno: None)
#     . GerarDataFramePayoff (Parâmetros: min_preco: float, max_preco: float, num_pontos: int = 100, cdi_anual: float = 0.0 | Retorno: pd.DataFrame)
#
# FUNÇÕES GLOBAIS:
#   CND (Parâmetros: X: float | Retorno: float)
#   BlackScholesOption (Parâmetros: CallPutFlag: str, S: float, X: float, T: float, r: float, v: float | Retorno: float)
#   NormalCurve (Parâmetros: X: float, mean: float, stdDev: float | Retorno: float)
#   CallPayoff (Parâmetros: sT: float, strike_price: float, premium: float | Retorno: float)
#   PutPayoff (Parâmetros: sT: float, strike_price: float, premium: float | Retorno: float)
#   sign (Parâmetros: x: float | Retorno: int)
#   roundto (Parâmetros: value: float, decimals: int | Retorno: float)
#   calcSigma (Parâmetros: ValorAtivo: float, Sigma: float, dias: int, indice: int | Retorno: float)
#
# ----------------------------


import math
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
from typing import List, Tuple, Optional, Dict, Any, Union, Callable, overload
from enum import Enum
from datetime import date, datetime
import pandas as pd
from Oplab import TOpLab, TOplabStockData
import json

DIAS_UTEIS_NO_ANO = 251

# Assuming MarketOptions, Vcl.Grids, VclTee.Chart, System.Generics.Collections are replaced by Python equivalents.
# For simplicity, we'll use basic lists and classes.
# TChart, TStringGrid will be simulated or replaced by print statements for data.

# --- Helper classes/functions (Delphi equivalents) ---


class TOptionStatus(Enum):
    ATIVA = "Ativa"
    DISP_NA = "N/A"
    ZERADA = "Zerada"
    EXPIRADA = "Expirada"

class TOption:
    def __init__(self):
        self.AtivoBase: str = ""
        self.Quant: int = 0
        self.Tipo: str = ""  # 'call', 'put', 'ativo'
        self.Oper: str = ""  # 'C' (Compra), 'V' (Venda)
        self.strikeN: float = 0.0
        self.strike: float = float('nan') # Começa como NaN
        self.Vencimento: Any = float('nan') # Começa como NaN (usar Any para aceitar int/float)
        self.DataVencimento: Optional[date] = None # Deve ser a data real de vencimento
        self.DataValorLast: Optional[date] = None  # Data da última cotação
        self.Nome: str = ""
        self.ValorIn: float = 0.0
        self.ValorOut: float = 0.0
        self.ValorNow: float = 0.0
        self.ValorBid: float = 0.0
        self.ValorAsk: float = 0.0
        self.ValorLast: float = 0.0
        self.TransacaoValue: float = 0.0
        self.Valorspread: float = 0.0
        self.Volatilidade: float = 0.0
        self.Serie: int = 0
        self.id: str = ""
        self.Status: TOptionStatus = TOptionStatus.ATIVA
        #self.expirada: bool = False

    def CopyOptionTo(self, target_option: 'TOption'):
        # Corrigido: Usar getattr para lidar com propriedades que podem ser None em targets mais antigos
        target_option.AtivoBase = self.AtivoBase
        target_option.Quant = self.Quant
        target_option.Tipo = self.Tipo
        target_option.Oper = self.Oper
        target_option.strikeN = self.strikeN
        target_option.strike = self.strike
        target_option.Vencimento = self.Vencimento
        target_option.DataVencimento = self.DataVencimento
        target_option.DataValorLast = self.DataValorLast
        target_option.Nome = self.Nome
        target_option.ValorIn = self.ValorIn
        target_option.ValorOut = self.ValorOut
        target_option.ValorNow = self.ValorNow
        target_option.ValorBid = self.ValorBid
        target_option.ValorAsk = self.ValorAsk
        target_option.ValorLast = self.ValorLast
        target_option.Valorspread = self.Valorspread
        target_option.Volatilidade = self.Volatilidade
        target_option.Serie = self.Serie
        target_option.id = self.id
        target_option.Status = self.Status
        #target_option.expirada = self.expirada

    # Simulates BlackScholesSimple for a single option
    def BlackScholesSimple(self, S: float, dias_para_vencimento: int, r_anual: float, v_anual: float) -> float:
        # Garante que as funções necessárias existam no escopo do módulo
        global DIAS_UTEIS_NO_ANO, BlackScholesOption

        call_put_flag = 'C' if self.Tipo == 'call' else 'P'
        if dias_para_vencimento <= 0:
            dias_para_vencimento = 1e-10 # Evita divisão por zero

        time_in_years = dias_para_vencimento / DIAS_UTEIS_NO_ANO

        # Assume que BlackScholesOption está definido no escopo global
        option_premium = BlackScholesOption(call_put_flag, S, self.strike, time_in_years, r_anual, v_anual)

        return self.Quant * (option_premium - self.ValorIn)


class TObjectList(list): # Simplified TObjectList for demonstration
    @property
    def Last(self):
        if self:
            return self[-1]
        return None

    def Add(self, item):
        super().append(item)

    def Clear(self):
        for item in self:
            if hasattr(item, 'Destroy') and callable(item.Destroy):
                item.Destroy()
        super().clear()

    def Destroy(self):
        self.Clear() # Call Clear to free contained objects if they have a Destroy method.

# Dummy Chart and Grid classes for type hinting and to avoid errors
class TChart:
    def ClearChart(self):
        print("Chart cleared.")
    def CriarLegenda(self, *args):
        print("Legend created.")
    def criarSerie(self, name, *args):
        print(f"Series '{name}' created.")
        return TLineSeries(name)
    @property
    def View3D(self):
        return False
    @View3D.setter
    def View3D(self, value):
        pass # Not implementing 3D view for now

class TLineSeries:
    def __init__(self, name):
        self.Name = name
        self.XValues = []
        self.YValues = []
        # Simulate other properties
        self.Pen = type('Pen', (object,), {'Visible': False, 'Color': 'red'})()
        self.ShowInLegend = True
        self.Color = 'blue'
        self.VertAxis = None
        self.Title = ""

    def AddXY(self, x, y, label="", color=""):
        self.XValues.append(x)
        self.YValues.append(y)
        # print(f"Series '{self.Name}' added point: ({x}, {y})")

    def Addarray(self, x_values, y_values):
        self.XValues.extend(x_values)
        self.YValues.extend(y_values)


class TStringGrid:
    def __init__(self):
        self.cells = {}
        self._row_count = 0
        self._col_count = 0
        self.rows = {} # Simulating Rows property for CommaText
        self.fixed_cols = 0

    @property
    def RowCount(self):
        return self._row_count

    @RowCount.setter
    def RowCount(self, value):
        self._row_count = value

    @property
    def ColCount(self):
        return self._col_count

    @ColCount.setter
    def ColCount(self, value):
        self._col_count = value

    def Cells(self, col, row):
        if (col, row) not in self.cells:
            self.cells[(col, row)] = ""
        return _GridCell(self.cells, col, row)

    def AutoSizeColumns(self):
        print("Columns auto-sized.")

class _GridCell: # Helper to allow `Grid.Cells[x,y] = value` syntax
    def __init__(self, cells_dict, col, row):
        self._cells = cells_dict
        self._col = col
        self._row = row

    @property
    def value(self):
        return self._cells.get((self._col, self._row), "")

    @value.setter
    def value(self, val):
        self._cells[(self._col, self._row)] = str(val)

    def __str__(self):
        return self.value

# Simplified TStringList
class TStringList(list):
    @property
    def CommaText(self):
        return ",".join(self)

    def Add(self, item):
        super().append(item)


# Assuming TOpLab and TOplabStockData are external and will be provided
#class TOplabStockData:
#    def __init__(self):
#        self.Symbol: str = ""
#        self.bid: float = 0.0
#        self.ask: float = 0.0
#
#    def Destroy(self):
#        pass # No explicit destruction needed in Python for simple objects

#class TOpLab:
#    def Create(self):
#        print("TOpLab created.")
#        return self
#
#    def Destroy(self):
#        print("TOpLab destroyed.")
#
#    def CotarSymbolsplus(self, comma_separated_symbols: str, stock_data_array: List[TOplabStockData]):
#        # This is a mock implementation. In a real scenario, this would fetch live data.
#        symbols = comma_separated_symbols.split(',')
#        for symbol in symbols:
#            data = TOplabStockData()
#            data.Symbol = symbol
#            # Mock data
#            data.bid = 100.0 + hash(symbol) % 10 - 5
#            data.ask = 100.0 + hash(symbol) % 10 - 5 + 0.5
#            stock_data_array.append(data)


# --- Constants ---
DIAS_UTEIS_NO_ANO = 251
NUM_DESVIOS_PADRAO_INTEGRACAO = 3.5
PASSOS_POR_DESVIO_PADRAO = 50

# --- Helper functions ---

def CND(X: float) -> float:
    l = abs(X)
    k = 1 / (1 + 0.2316419 * l)
    result = 1 - 1 / math.sqrt(2 * math.pi) * math.exp(-math.pow(l, 2) / 2) * (
        0.31938153 * k + -0.356563782 * math.pow(k, 2) + 1.781477937 * math.pow(k, 3) +
        -1.821255978 * math.pow(k, 4) + 1.330274429 * math.pow(k, 5)
    )
    if X < 0:
        result = 1 - result
    return result

def BlackScholesOption(CallPutFlag: str, S: float, X: float, T: float, r: float, v: float) -> float:
    if T == 0:
        T = 1e-10

    if v == 0: # Handle zero volatility to avoid division by zero or log(0)
        if S > X:
            return S - X if CallPutFlag == 'C' else 0.0
        else:
            return X - S if CallPutFlag == 'P' else 0.0

    d1 = (math.log(S / X) + (r + math.pow(v, 2) / 2) * T) / (v * math.sqrt(T))
    d2 = d1 - v * math.sqrt(T)

    if CallPutFlag == 'C':
        return S * CND(d1) - X * math.exp(-r * T) * CND(d2)
    elif CallPutFlag == 'P':
        return X * math.exp(-r * T) * CND(-d2) - S * CND(-d1)
    return 0.0

def NormalCurve(X: float, mean: float, stdDev: float) -> float:
    if stdDev <= 1e-9:
        if abs(X - mean) < 1e-9:
            return float('inf') # Simulates MaxSingle for a Dirac delta function
        else:
            return 0.0
    variance = stdDev**2
    exponent = -(X - mean)**2 / (2 * variance)
    return (1 / (stdDev * math.sqrt(2 * math.pi))) * math.exp(exponent)

def CallPayoff(sT: float, strike_price: float, premium: float) -> float:
    return max(0, sT - strike_price) - premium

def PutPayoff(sT: float, strike_price: float, premium: float) -> float:
    return max(0, strike_price - sT) - premium

def sign(x: float) -> int:
    if x > 0: return 1
    if x < 0: return -1
    return 0

def roundto(value: float, decimals: int) -> float:
    factor = 10**(-decimals)
    return round(value / factor) * factor


# --- Enums ---

class TTipoAnalise(Enum):
    taNoPrimeiroVencimento = 0
    taAntecipada = 1

class TTipoCalculoPayoff(Enum):
    tcpFinal = 0
    tcpNoVencimento = 1
    tcpAntecipado = 2

# --- Records ---

class TAnaliseEsperancaData:
    def __init__(self):
        self.Precos: List[float] = []
        self.FDPValues: List[float] = []
        self.PayoffValues: List[float] = []
        self.HopeValues: List[float] = []
        self.EsperancaTotal: float = 0.0


class TCategoria(Enum):
    BOX_RENDA_FIXA        = "Box/Renda Fixa"
    COLLAR_CALENDARIO     = "Collar Calendário"
    COLLAR_POTENCIALIZADO = "Collar Potencializado"
    ESTRUTURAS_DIRECIONAIS = "Estruturas Direcionais"
    STRANGLES       = "Strangles"
    OUTRO = "Outro"
    SEM_CATEGORIA         = ""


#----------------------------------------------------------------------------------------------------------------
#----------------------------------------------------------------------------------------------------------------
# --- Class TEstrutura ---
#----------------------------------------------------------------------------------------------------------------
#----------------------------------------------------------------------------------------------------------------
# Est: TEstrutura
class TEstrutura:
    def __init__(self):
        self.Options: TObjectList[TOption] = TObjectList()
        # valores
        self.ValorIn: float = 0.0        # valor gasto (+) ou recebido (-) para entrar na operação
        self.ValorLucro: float = 0.0     # valorOut - valorIn
        self.ValorOffset: float = 0.0
        self.ValorAim: float = 0.0       # valor alvo para a venda da estrutura
        self.ValorOut: float = 0.0       # Valor de Desmontagem Total
        self.ValorNow: float = 0.0       # valor atual da estrutura (+) comprado (-) vendido
        # ativo
        self.AtivoBase: str = ""
        self.ValorAtivoIn: float = 0.0   # cotação inicial do ativo
        self.ValorAtivoNow: float = 0.0  # Preço Spot Atual (Em tempo real)
        # propriedades
        self.id: str = ""
        self.Sigma: float = 0.0
        self.Nome: str = ""
        self.MaiorVencimento: int = 0
        self.Info: str = ""
        self.indice: int = 0
        self.QuantBase: int = 1
        self.Categoria: TCategoria = TCategoria.SEM_CATEGORIA
        self.Corretora: str = ""

        self.DTE: int = 0                # dias para expiração na montagem
        self.LucroAim: float = 0.0       # lucro alvo para a venda da estrutura
        #self.LucroNow: float = 0.0
        self.LucroOut: float = 0.0       # lucro de Desmontagem Total
        self.LucroAimPercentual: float = 0.0 # percentual atual do lucro alvo
        self.MenorVencimento: int = 0

    def Destroy(self):
        self.Options.Destroy() # Ensures options are also destroyed if they have a Destroy method

    # --- Private Methods ---


    def calcular_dados(self):
        self.MenorVencimento = self.GetMenorVencimento()
        self.LucroAim = self.ValorAim - self.ValorIn * self.QuantBase
        self.ValorLucro = self.ValorNow - self.ValorIn * self.QuantBase
        #self.LucroNow = self.ValorNow - self.ValorIn * self.QuantBase


    def imprimir(self):
        """Imprime os dados da estrutura linha a linha e as pernas em linha única."""
        print(f"\n{'-'*30} DADOS DA ESTRUTURA {'-'*30}")

        # 1. Imprime propriedades da Estrutura (Uma por linha)
        # Filtramos 'Options' para imprimir separadamente depois
        for attr, value in vars(self).items():
            if attr != 'Options':
                # Formata floats para 4 casas para facilitar a leitura técnica
                val_display = f"{value:.4f}" if isinstance(value, float) else value
                print(f"{attr}: {val_display}")

        print(f"\n{'-'*30} OPÇÕES (PERNAS) {'-'*30}")

        # 2. Imprime propriedades das Opções (Cada perna em uma única linha)
        if not self.Options:
            print("Nenhuma opção encontrada nesta estrutura.")
        else:
            for i, opt in enumerate(self.Options):
                # Coleta todos os atributos da opção e formata em uma string separada por " | "
                opt_attrs = vars(opt)
                linha_opt = []
                for k, v in opt_attrs.items():
                    v_fmt = f"{v:.4f}" if isinstance(v, float) else v
                    linha_opt.append(f"{k}: {v_fmt}")

                print(f"Perna {i+1:02d} >> " + " | ".join(linha_opt))

        print(f"{'-'*78}\n")




    def VerificarOpcoesZeradas(self):
        """
        Percorre as pernas da estrutura. Se o somatório global de quantidade
        de um Ticker for 0, marca o status como ZERADA.
        """
        if not estrutura or not estrutura.Options:
            return

        # 1. Dicionário para somar quantidades por nome do papel
        totais_por_ticker = {}

        for op in estrutura.Options:
            nome = op.Nome.upper().strip()
            # Soma a quantidade (considerando o sinal de compra/venda)
            totais_por_ticker[nome] = totais_por_ticker.get(nome, 0) + op.Quant

        # 2. Percorre novamente para atualizar o status
        for op in estrutura.Options:
            nome = op.Nome.upper().strip()

            # Se o total global desse papel for 0, marcamos como ZERADA
            # Ou se a perna individualmente tiver quantidade 0
            if totais_por_ticker[nome] == 0 or op.Quant == 0:
                op.Status = TOptionStatus.ZERADA
                # op.ValorNow = 0.0 # Opcional: já garante que não afeta o P&L atual




    def txt(self) -> str:
        """Gera uma string formatada com os dados da estrutura e suas pernas."""
        linhas = [f"{'='*20} ESTRUTURA {'='*20}"]

        for attr, value in vars(self).items():
            if attr != 'Options':
                # Trata Enum, float, e outros tipos
                if hasattr(value, 'value'):  # Enum
                    val_fmt = value.value
                elif isinstance(value, float):
                    val_fmt = f"{value:.4f}"
                else:
                    val_fmt = value
                linhas.append(f"{attr}: {val_fmt}")

        linhas.append(f"{'-'*15} OPÇÕES {'-'*15}")

        for i, opt in enumerate(self.Options):
            p_data = []
            for k, v in vars(opt).items():
                if hasattr(v, 'value'):      # Enum
                    v_fmt = v.value
                elif isinstance(v, float):
                    v_fmt = f"{v:.4f}"
                else:
                    v_fmt = v
                p_data.append(f"{k}: {v_fmt}")
            linhas.append(f"Perna {i+1:02d} >> " + " | ".join(p_data))

        linhas.append("="*51)
        return "\n".join(linhas)

##
##    def txt(self) -> str:
##        """Gera uma string formatada com os dados da estrutura e suas pernas."""
##        linhas = [f"{'='*20} ESTRUTURA {'='*20}"]
##
##        # 1. Propriedades da Estrutura (Uma por linha)
##        for attr, value in vars(self).items():
##            if attr != 'Options':
##                val_fmt = f"{value:.4f}" if isinstance(value, float) else value
##                linhas.append(f"{attr}: {val_fmt}")
##
##        linhas.append(f"{'-'*15} OPÇÕES {'-'*15}")
##
##        # 2. Propriedades das Opções (Tudo em uma linha por opção)
##        for i, opt in enumerate(self.Options):
##            # Coleta atributos da perna e monta a linha única
##            p_data = [f"{k}: {f'{v:.4f}' if isinstance(v, float) else v}"
##                     for k, v in vars(opt).items()]
##            linhas.append(f"Perna {i+1:02d} >> " + " | ".join(p_data))
##
##        linhas.append("="*51)
##        return "\n".join(linhas)

#----------------------------------------------------------------------------------------------------------------
    def _GerarDistribuicaoDeResultados(self, mean: float, volatility: float, CDI: float,
                                      Tipo: TTipoAnalise, diasAntesDoPrimeiroVenc: int,
                                      AnaliseData: TAnaliseEsperancaData):
        listaPrecos = []
        listaFDPs = []
        listaPayoffs = []
        listaHopes = []

        step = volatility / PASSOS_POR_DESVIO_PADRAO
        X = mean - NUM_DESVIOS_PADRAO_INTEGRACAO * volatility
        hopeTotal = 0.0

        while X < mean + NUM_DESVIOS_PADRAO_INTEGRACAO * volatility:
            if X > 0:
                FDPValue = NormalCurve(X, mean, volatility)
                PayoffValue = 0.0

                if Tipo == TTipoAnalise.taNoPrimeiroVencimento:
                    PayoffValue = self.CalcPayoffnoVencimento(X, CDI)
                elif Tipo == TTipoAnalise.taAntecipada:
                    PayoffValue = self.CalcPayoffAntecipado(X, diasAntesDoPrimeiroVenc, CDI)

                listaPrecos.append(X)
                listaFDPs.append(FDPValue)
                listaPayoffs.append(PayoffValue)

                hopeContribution = FDPValue * PayoffValue
                listaHopes.append(hopeContribution)
                hopeTotal += hopeContribution
            X += step

        AnaliseData.Precos = listaPrecos
        AnaliseData.FDPValues = listaFDPs
        AnaliseData.PayoffValues = listaPayoffs
        AnaliseData.HopeValues = listaHopes
        AnaliseData.EsperancaTotal = hopeTotal * step

    # No arquivo Estrutura.py, dentro da classe TEstrutura:

# No arquivo Estrutura.py, dentro da classe TEstrutura:
# Certifique-se de que os imports pd, StringIO e TOption estão acessíveis no módulo.
#----------------------------------------------------------------------------------------------------------------
    @classmethod
    def DecodificarPapelOpcao(cls, papel: str) -> Tuple[str, str, float, str]:
        papel = papel.strip().upper()

        # Se na posição 4 não for letra, é Ativo (PETR4, BOVA11, VALE3)
        if len(papel) < 5 or not papel[4].isalpha():
            # TIPO: 'ativo', ATIVO_BASE: o nome completo (BOVA11)
            return 'ativo', papel, 0.0, ""

        # Se for Opção
        base_ativo = papel[:4] # PETR
        letra_serie = papel[4] # K
        tipo = 'call' if 'A' <= letra_serie <= 'L' else 'put'
        return tipo, base_ativo, 0.0, letra_serie

#----------------------------------------------------------------------------------------------------------------
    @classmethod
    def ImportarEstruturaDeStringTransacoes(cls, dados_string: str) -> 'TEstrutura':
        """
        Importa transações de uma string/clipboard, decodifica papéis em opções/ativos.

        op.ValorIn = Preço Unitário da Transação.
        estrutura.ValorIn = Custo Financeiro Líquido Total.
        """
        estrutura = cls()

        try:
            from io import StringIO
            # Tenta ler com separador de tab; 'sep=\s+' pode ser mais robusto para espaços/tabs
            df = pd.read_csv(StringIO(dados_string), sep='\t', decimal=',')
            #print("Lido com separador de tabulação.")
            #print(df)
            df.columns = df.columns.str.strip()
        except Exception as e:
            print(f"Erro ao ler a string de transações: {e}")
            return estrutura

        # --- VERIFICAÇÃO INICIAL CRÍTICA ---
        if df.empty:
            print("Erro de leitura: DataFrame vazio após leitura do CSV.")
            return estrutura

        colunas_necessarias = ['Oper', 'Papel', 'Quant', 'Preço']
        if not all(col in df.columns for col in colunas_necessarias):
             print(f"Erro: Colunas faltando. Necessárias: {colunas_necessarias}")
             return estrutura

        # --- BLOCO DE LIMPEZA E CONVERSÃO DE TIPO CRÍTICO ---
        # 1. Limpar espaços
        for col in ['Papel', 'Preço', 'Quant']:
            if col in df.columns:
                df[col] = df[col].astype(str).str.strip()

        #print("DataFrame após limpeza inicial:")
        #print(df)

        # 2. Corrigir e converter 'Preço' para float
        df['Preço'] = pd.to_numeric(df['Preço'].str.replace(',', '.', regex=False), errors='coerce').fillna(0.0)

        #print("DataFrame após conversão de 'Preço':")
        #print(df)

        # 3. Corrigir e converter 'Quant' para int
        df['Quant'] = pd.to_numeric(df['Quant'], errors='coerce').fillna(0).astype(int)
        # -------------------------------------------------------------------------

        # 1. Pré-processamento e decodificação do papel
        df['Oper'] = df['Oper'].str.strip().str.lower()
        df[['Tipo', 'AtivoBase', 'Strike', 'Serie_Letra']] = df['Papel'].apply(
            lambda x: pd.Series(cls.DecodificarPapelOpcao(x))
        )

        #print("DataFrame após decodificação dos papéis:")
        #print(df)

        # 2. Calcula a Quantidade Sinalizada e o Valor Financeiro da Transação
        df['Quant_Sinalizada'] = df.apply(
            lambda row: row['Quant'] if row['Oper'] in ['compra', 'c'] else -row['Quant'],
            axis=1
        )
        df['Financeiro_Transacao'] = df['Quant_Sinalizada'] * df['Preço']

        #print("DataFrame após cálculo de Quant_Sinalizada e Financeiro_Transacao:")
        #print(df)

        custo_total_estrutura = 0.0

        # 3. Itera sobre CADA LINHA e cria uma perna de opção
        for index, row in df.iterrows():
            quant_sinalizada = row['Quant_Sinalizada']

            if quant_sinalizada == 0:
                continue

            op = TOption()
            op.Quant = int(quant_sinalizada)
            op.Tipo = row['Tipo']
            op.Oper = 'C' if quant_sinalizada > 0 else 'V'
            op.Nome = row['Papel']
            op.ValorIn = row['Preço']
            op.ValorAsk = row['Preço']
            op.ValorBid = row['Preço']
            op.TransacaoValue = row['Preço']
            #print(f"Adicionando opção: Quant={op.Quant}, Tipo={op.Tipo}, Oper={op.Oper}, Nome={op.Nome}, ValorIn={op.ValorIn}")

            # CAMPOS VAZIOS / A SEREM OBTIDOS DEPOIS
            if op.Tipo == 'ativo':
                op.strike = float('nan')
                op.Vencimento = float('nan') # Para que o F() mostre ---
            else:
                op.strike = 0.0
                op.Vencimento = 0

            op.Volatilidade = 0.0

            estrutura.Options.Add(op)
            custo_total_estrutura += row['Financeiro_Transacao']

            # 4. Define o Ativo Base e Preço de Referência
            if estrutura.AtivoBase == "":
                 estrutura.AtivoBase = row['AtivoBase']
                 # Se a primeira perna for um ativo, usa o preço da transação como ValorAtivo (preço atual)
                 if row['Tipo'] == 'ativo':
                    estrutura.ValorAtivoIn = row['Preço']
                 else:
                    estrutura.ValorAtivoIn = 0.0 # Será cotado depois

        #print("Estrutura importada com sucesso.")
        #print(df)

        # 5. Finaliza a estrutura
        estrutura.ValorIn = custo_total_estrutura

        if estrutura.Options:
            estrutura.QuantBase = abs(estrutura.Options[0].Quant)

        qb = max(1, estrutura.QuantBase)
        # Transforma o custo TOTAL da importação em UNITÁRIO
        estrutura.ValorIn = custo_total_estrutura / qb

        #print()
        return estrutura


#----------------------------------------------------------------------------------------------------------------
    def _ExecutarAnaliseLucroEsperado(self, Sigma: float, mean: float, N_dias_projecao: int, CDI: float,
                                      Tipo: TTipoAnalise, diasAntesDoPrimeiroVenc: int, Chart: Optional[TChart],
                                      PlotExpectedProfit: bool, PlotProfit: bool, PlotSigmas: bool,
                                      PlotFDP: bool, PlotAtivo: bool, limiteMin: float, limiteMax: float) -> float:
        plotGraph = (Chart is not None)

        if N_dias_projecao <= 0:
            N_dias_projecao = 1

        volatility = (Sigma * math.sqrt(N_dias_projecao)) * mean
        step = volatility / PASSOS_POR_DESVIO_PADRAO

        analise_data = TAnaliseEsperancaData()
        self._GerarDistribuicaoDeResultados(mean, volatility, CDI, Tipo, diasAntesDoPrimeiroVenc, analise_data)

        result = self._CalcularEsperancaNaFaixa(analise_data, limiteMin, limiteMax, step)

        if plotGraph:
            Chart.ClearChart()
            self._PlotarDadosCalculoEsperanca(Chart, analise_data, limiteMin, limiteMax, PlotFDP, PlotProfit, PlotExpectedProfit)

            if PlotSigmas:
                serie = Chart.criarSerie('Sigmas', 3, "pscircle", False)
                serie.Pen.Visible = False
                serie.ShowInLegend = False
                serie.Color = "clRed"
                for i in range(7):
                    serie.AddXY(mean + (i - 3) * volatility, 0, '', "clTeeColor")

            if PlotAtivo:
                serie = Chart.criarSerie('Ativo', 5, "psrectangle", False)
                serie.Pen.Visible = False
                serie.ShowInLegend = False
                serie.Color = "clRed"
                serie.AddXY(mean, 0, '', "clTeeColor")
        return result

#----------------------------------------------------------------------------------------------------------------
    def _CalcularEsperancaNaFaixa(self, AnaliseData: TAnaliseEsperancaData, limiteMin: float, limiteMax: float, step: float) -> float:
        if (limiteMin == -1) and (limiteMax == -1):
            return AnaliseData.EsperancaTotal

        hopeTotal = 0.0
        for i in range(len(AnaliseData.Precos)):
            if ((limiteMin == -1) or (AnaliseData.Precos[i] >= limiteMin)) and \
               ((limiteMax == -1) or (AnaliseData.Precos[i] <= limiteMax)):
                hopeTotal += AnaliseData.HopeValues[i]
        return hopeTotal * step

#----------------------------------------------------------------------------------------------------------------
    def _PlotarDadosCalculoEsperanca(self, Chart: TChart, AnaliseData: TAnaliseEsperancaData,
                                    limiteMin: float, limiteMax: float, PlotFDP: bool,
                                    PlotProfit: bool, PlotExpectedProfit: bool):
        if PlotFDP:
            serieFDP = Chart.criarSerie('F.D.P.', 5, "psNothing", False)
            serieFDP.Addarray(AnaliseData.Precos, AnaliseData.FDPValues)
        if PlotProfit:
            serieProfit = Chart.criarSerie('Profit', 5, "psNothing", False)
            serieProfit.Addarray(AnaliseData.Precos, AnaliseData.PayoffValues)
        if PlotExpectedProfit:
            serieHope = Chart.criarSerie('Expected Profit', 5, "psNothing", False)
            for i in range(len(AnaliseData.Precos)):
                if ((limiteMin == -1) or (AnaliseData.Precos[i] >= limiteMin)) and \
                   ((limiteMax == -1) or (AnaliseData.Precos[i] <= limiteMax)):
                    serieHope.AddXY(AnaliseData.Precos[i], AnaliseData.HopeValues[i], '', "clTeeColor")
                else:
                    serieHope.AddXY(AnaliseData.Precos[i], 0, '', "clTeeColor")

#----------------------------------------------------------------------------------------------------------------
    def _PrecificarOpcaoBS(self, Opcao: TOption, PrecoAtivo: float, diasParaVencimento: int, CDI: float) -> float:
        if Opcao.Tipo == 'call':
            call_put_flag = 'C'
        elif Opcao.Tipo == 'put':
            call_put_flag = 'P'
        else:
            return 0.0 # Not an option

        if diasParaVencimento <= 0:
            diasParaVencimento = 1e-10

        tempoEmAnos = diasParaVencimento / DIAS_UTEIS_NO_ANO
        return BlackScholesOption(call_put_flag, PrecoAtivo, Opcao.strike, tempoEmAnos, CDI, Opcao.Volatilidade)

    # --- Public Methods ---
#----------------------------------------------------------------------------------------------------------------
# Dentro da sua classe TEstrutura:

# Dentro da sua classe TEstrutura:

    def PlotarPayoffNoVencimento_Matplotlib_FIG(
        self, # <--- Recebe a instância da estrutura
        minv: float,
        maxv: float,
        CDI: float,
        plotarStrikes: bool,
        plotarValorAtivo: bool,
        plotarSigmas: bool
    ) -> Any: # Retorna um objeto Figure
        """
        [MÉTODO ADAPTADO] Simula o PlotarPayoffNoVencimento, gerando uma figura Matplotlib.
        O objeto Figure é retornado para ser processado pelo mpld3.
        """
        import numpy as np
        import pandas as pd
        import matplotlib.pyplot as plt

        # 1. Geração dos Pontos (Dados)
        precos_base = np.linspace(minv, maxv, 200)
        strikes = sorted(list(set([op.strike for op in self.Options if op.Tipo in ('call', 'put')])))
        precos_finais = sorted(list(set(list(precos_base) + strikes)))

        payoffs = [self.CalcPayoffnoVencimento(preco, CDI) for preco in precos_finais]

        # 2. Criação da Figura Matplotlib
        fig, ax = plt.subplots(figsize=(10, 6))

        # Plota a linha principal
        ax.plot(precos_finais, payoffs, label='Payoff', color='blue', linewidth=2)

        # Título e Rótulos
        ax.set_title(f"Payoff: {self.Nome} (@ {self.GetMenorVencimento()} dias)")
        ax.set_xlabel('Preço do Ativo')
        ax.set_ylabel('Lucro/Prejuízo (P&L)')
        ax.grid(True, linestyle='--', alpha=0.7)

        # 3. Adicionar Linhas de Referência usando a função auxiliar (Plotar_Strikes_ValorAtivo_Sigma)
        self.Plotar_Strikes_ValorAtivo_Sigma( # Assumindo que esta função está no TEstrutura
            ax,
            plotarStrikes=plotarStrikes,
            plotarValorAtivo=plotarValorAtivo,
            plotarSigmas=plotarSigmas
        )

        # Linha de Zero
        ax.axhline(0, color='black', linestyle='--', linewidth=1, alpha=0.8)

        # 4. Retorna o objeto Figure (IMPORTANTE!)
        return fig

    def PlotarPayoffNoVencimento_Matplotlib_SVG(
        self,
        minv: float,
        maxv: float,
        CDI: float,
        plotarStrikes: bool,
        plotarValorAtivo: bool,
        plotarSigmas: bool
    ) -> str:
        import numpy as np
        import matplotlib.pyplot as plt
        from io import BytesIO

        # --- AJUSTE DE VOLATILIDADE ---
        # Garantimos que a curva "Now" apareça mesmo se os dados da API vierem zerados
        for op in self.Options:
            if op.Tipo != 'ativo':
                # Se a opção está zerada, tenta usar a vol da estrutura.
                # Se a da estrutura também for 0, usa o fallback de 0.20 (20%)
                if op.Volatilidade <= 0:
                    op.Volatilidade = self.Sigma if self.Sigma > 0.25 else 0.25
                    print('op.Volatilidade: ',op.Volatilidade)

        # 1. Preparação de Pontos
        precos_base = np.linspace(minv, maxv, 200)
        strikes = sorted(list(set([op.strike for op in self.Options if op.strike > 0])))
        precos_finais = sorted(list(set(list(precos_base) + strikes)))

        fig, ax = plt.subplots(figsize=(10, 6))

        # --- CURVA 1: "THEN" (Vencimento - Tracejada) ---
        payoffs_venc = [self.CalcPayoffnoVencimento(preco, CDI) for preco in precos_finais]
        ax.plot(precos_finais, payoffs_venc, label='Vencimento', color='blue', linestyle='-', alpha=1, linewidth=2.0)

        # --- CURVA 2: "NOW" (Agora - Sólida) ---
        # Agora o BlackScholesSimple terá volatilidade para calcular a curva suave
        payoffs_now = [self.BlackScholesSimple(preco, 0, CDI) for preco in precos_finais]
        ax.plot(precos_finais, payoffs_now, label='Agora (T+0)', color='blue', linestyle='--', linewidth=1.5)

        # 2. Configurações de Design
        ax.set_xlim(minv, maxv)
        ax.set_title(f"Payoff: {self.Nome} (@ {self.GetMenorVencimento()} dias)")
        ax.set_xlabel('Preço do Ativo')
        ax.set_ylabel('Lucro/Prejuízo (P&L)')
        ax.grid(True, linestyle='--', alpha=0.5)

        # 3. Linhas de Referência (Strikes, Spot, Sigmas)
        self.Plotar_Strikes_ValorAtivo_Sigma_Matplotlib_SVG(
            ax,
            plotarStrikes=plotarStrikes,
            plotarValorAtivo=plotarValorAtivo,
            plotarSigmas=plotarSigmas
        )

        ax.axhline(0, color='black', linestyle='-', linewidth=1, alpha=0.3)
        ax.legend(loc='best')

        fig.tight_layout()
        buf = BytesIO()
        plt.savefig(buf, format='svg', bbox_inches='tight', pad_inches=0.02)
        plt.close(fig)
        return buf.getvalue().decode('utf-8')



#----------------------------------------------------------------------------------------------------------------


#----------------------------------------------------------------------------------------------------------------
    def PlotarPayoffNoVencimento(
        self, # <--- CORRIGIDO: Apenas self é necessário
        minv: float,
        maxv: float,
        CDI: float,
        plotarStrikes: bool,
        plotarValorAtivo: bool,
        plotarSigmas: bool
    ):
        """
        Simula o PlotarPayoffNoVencimento do Delphi, usando Matplotlib para visualização.
        Usa os dados da própria instância (self) e chama o método Plotar_Strikes_ValorAtivo_Sigma.
        """
        import numpy as np
        import matplotlib.pyplot as plt
        import matplotlib.colors as mcolors
        # Assumindo a constante DIAS_UTEIS_NO_ANO = 251 está definida no módulo

        # 1. Configuração do Gráfico
        fig, ax = plt.subplots(figsize=(10, 6))
        # Usa self.Nome e self.GetMenorVencimento()
        ax.set_title(f"Payoff: {self.Nome} (@ {self.GetMenorVencimento()} dias)")
        ax.set_xlabel("Preço do Ativo no Vencimento (R$)")
        ax.set_ylabel("Lucro/Prejuízo Total (R$)")
        ax.axhline(0, color='gray', linestyle='-', linewidth=1.0) # Linha de zero
        ax.grid(True, linestyle='--', alpha=0.6)

        # 2. Geração dos Pontos (Preços e Payoffs)
        precos_base = np.linspace(minv, maxv, 200)

        # Extraindo e unindo os strikes (Corrigido da última iteração)
        strikes = sorted(list(set([op.strike for op in self.Options if op.Tipo in ('call', 'put')])))
        # Evita TypeError ao garantir que ambos os lados são listas antes de unir
        precos_finais = sorted(list(set(list(precos_base) + strikes)))

        # Cálculo do Payoff
        payoffs = [self.CalcPayoffnoVencimento(preco, CDI) for preco in precos_finais]

        # 3. Plotagem da Curva Principal
        ax.plot(precos_finais, payoffs, label='Payoff no Vencimento', color=mcolors.TABLEAU_COLORS['tab:blue'], linewidth=2)

        # 4. Plotagem de Itens de Referência (CHAMADA AO NOVO MÉTODO)
        # O self.Plotar_Strikes_ValorAtivo_Sigma deve ser chamado com o objeto 'ax'
        self.Plotar_Strikes_ValorAtivo_Sigma(ax, plotarStrikes, plotarValorAtivo, plotarSigmas)

        ax.legend()
        plt.show()
#    def PlotarPayoffNoVencimento_Matplotlib(
#        self, # <--- Recebe a instância da estrutura
#        estrutura: 'TEstrutura',
#        minv: float,
#        maxv: float,
#        CDI: float,
#        plotarStrikes: bool,
#        plotarValorAtivo: bool,
#        plotarSigmas: bool
#    ):
#        """
#        Simula o PlotarPayoffNoVencimento do Delphi, usando Matplotlib para visualização.
#        A lógica de cálculo é a mesma da TEstrutura.
#        """
#
#        # 1. Configuração do Gráfico
#        fig, ax = plt.subplots(figsize=(10, 6))
#        ax.set_title(f"Payoff: {estrutura.Nome} (@ {estrutura.GetMenorVencimento()} dias)")
#        ax.set_xlabel("Preço do Ativo no Vencimento (R$)")
#        ax.set_ylabel("Lucro/Prejuízo Total (R$)")
#        ax.grid(True, linestyle='--', alpha=0.6)
#
#        # 2. Geração dos Pontos (Preços e Payoffs)
#        num_pontos = 200
#        precos = np.linspace(minv, maxv, num_pontos)
#
#        # Extraindo os strikes da estrutura
#        strikes = sorted(list(set([op.strike for op in estrutura.Options if op.Tipo in ('call', 'put')])))
#
#        # Adicionando os strikes aos preços gerados
#        precos = sorted(list(set(list(precos) + strikes)))
#
#        # Cálculo do Payoff (Chama o método real da estrutura)
#        payoffs = [estrutura.CalcPayoffnoVencimento(preco, CDI) for preco in precos]
#
#        # 3. Plotagem da Curva Principal (Payoff)
#        ax.plot(precos, payoffs, label='Payoff no Vencimento', color='blue', linewidth=2)
#
#        # Adiciona a linha de zero lucro/prejuízo
#        ax.axhline(0, color='gray', linestyle='-', linewidth=1.0)
#
#        # 4. Plotagem de Itens de Referência (Substituindo criarSerie/AddXY)
#
#        # 4.a. Plotar Strikes (substitui a série de Triângulos/Círculos)
#        if plotarStrikes:
#            strikes = sorted(list(set([op.strike for op in estrutura.Options if op.Tipo in ('call', 'put')])))
#            y_min, y_max = ax.get_ylim()
#            for strike in strikes:
#                ax.axvline(strike, color='red', linestyle=':', linewidth=0.8, alpha=0.7)
#                # Adiciona o texto do Strike
#                ax.text(strike, y_min * 0.9 + y_max * 0.1, f'{strike:.2f}',
#                        rotation=90, verticalalignment='bottom', fontsize=9, color='red', alpha=0.7)
#
#        # 4.b. Plotar Valor Ativo (Preço Spot)
#        if plotarValorAtivo and estrutura.ValorAtivo > 0:
#            ax.axvline(estrutura.ValorAtivo, color='green', linestyle='-', linewidth=1.5, label=f'Preço Spot ({estrutura.ValorAtivo:.2f})')
#
#        # 4.c. Plotar Sigmas (Simulação dos Desvios Padrão)
#        if plotarSigmas and estrutura.Sigma > 0 and estrutura.ValorAtivo > 0:
#            # A lógica de Sigma para plotagem é mais complexa e depende de quantos dias
#            dias_projecao = estrutura.GetMenorVencimento()
#            if dias_projecao <= 0: dias_projecao = 21 # Fallback para 21 dias úteis
#
#            # Volatilidade em R$ (Sigma * Preço * Sqrt(Dias/DIAS_UTEIS_NO_ANO))
#            DIAS_UTEIS_NO_ANO = 251 # Assumindo a constante
#            vol_reais = estrutura.Sigma * estrutura.ValorAtivo * np.sqrt(dias_projecao / DIAS_UTEIS_NO_ANO)
#
#            for i in range(-3, 4): # -3 Sigma a +3 Sigma
#                sigma_val = estrutura.ValorAtivo + i * vol_reais
#                ax.axvline(sigma_val, color='orange', linestyle='--', linewidth=0.5, alpha=0.6)
#                ax.text(sigma_val, 0, f'{i}σ', verticalalignment='top', fontsize=8, color='orange')
#
#        ax.legend()
#        plt.show()



#   def PlotarPayoffNoVencimento(self, Chart: TChart, minv: float, maxv: float, CDI: float = 0.0,
#                                 plotarStrikes: bool = True, plotarValorAtivo: bool = True,
#                                 plotarSigmas: bool = True):
#        Chart.View3D = False
#        Chart.CriarLegenda("laLeft", 50, 30, True)
#
#        self.FixValorIn()
#        self.Plotar_Strikes_ValorAtivo_Sigma(Chart, plotarStrikes, plotarValorAtivo, plotarSigmas)
#
#        maior_venc, menor_venc = self.GetVencimentos()
#        bVencimentoUnico = (menor_venc == float('inf')) or (maior_venc == menor_venc)
#
#        serie = Chart.criarSerie('Payoff no Vencimento')
#        serie.VertAxis = "aleftaxis"
#
#        lista_de_precos: List[float] = []
#        lista_de_precos.append(minv)
#        lista_de_precos.append(maxv)
#
#        for option in self.Options:
#            if (option.Tipo != 'ativo') and (option.strike > 0):
#                if option.strike not in lista_de_precos: # Check if strike is already in the list
#                    lista_de_precos.append(option.strike)
#
#        if bVencimentoUnico:
#            serie.Title = 'Payoff no Vencimento (Final)'
#        else:
#            serie.Title = 'Payoff no 1º Vencimento'
#            for i in range(101): # 0 to 100
#                preco_intermediario = minv + i * (maxv - minv) / 100
#                if preco_intermediario not in lista_de_precos:
#                    lista_de_precos.append(preco_intermediario)
#
#        lista_de_precos.sort()
#
#        for preco in lista_de_precos:
#            serie.AddXY(preco, self.CalcPayoffnoVencimento(preco, CDI))

    # Overloads - Python doesn't have direct overloads like Delphi,
    # so we use default arguments and check types/number of args if needed.
    # The last defined method with the most arguments will be the "main" one.
    # We simulate overloads by having a single method with default parameters.

#----------------------------------------------------------------------------------------------------------------
    def PlotarEstruturaNow(self, Chart: TChart, minv: float, maxv: float, CDI: float, plotarStrikes: bool):
        Chart.View3D = False
        Chart.CriarLegenda("laLeft", 50, 30, True)
        self.FixValorIn()
        self.Plotar_Strikes_ValorAtivo_Sigma(Chart, plotarStrikes, plotarStrikes, plotarStrikes)

        lista_strikes: List[float] = []
        for option in self.Options:
            if option.Tipo != 'ativo':
                lista_strikes.append(option.strike)
        lista_strikes.sort()

        for i in range(101):
            lista_strikes.append(minv + i * (maxv - minv) / 100)
        lista_strikes.sort()

        serie = Chart.criarSerie('Now')
        serie.VertAxis = "aleftaxis"
        for strike in lista_strikes:
            serie.AddXY(strike, self.BlackScholesSimple(strike, 0, CDI), '', "clTeeColor")

#----------------------------------------------------------------------------------------------------------------
    def PlotarEstruturaThen(self, Chart: TChart, minv: float, maxv: float, CDI: float, plotarStrikes: bool):
        Chart.View3D = False
        Chart.CriarLegenda("laLeft", 50, 30, True)

        maior_venc, menor_venc = self.GetVencimentos()

        estrutura_lcl = TEstrutura()
        self.CopyEstruturaTo(estrutura_lcl)
        estrutura_lcl.Options.Clear()

        for option in self.Options:
            if (option.Vencimento == maior_venc) or (option.Tipo == 'ativo'):
                new_option = TOption()
                option.CopyOptionTo(new_option)
                estrutura_lcl.Options.Add(new_option)

        self_valor_in = self.CalcValorIn()
        estrutura_lcl_valor_in = estrutura_lcl.CalcValorIn()

        if (self_valor_in != estrutura_lcl_valor_in) and (len(estrutura_lcl.Options) > 0):
            # This logic attempts to adjust the 'ValorIn' of the first option
            # to compensate for the difference in total 'ValorIn' when options are filtered.
            # This is a bit unusual and might need re-evaluation in a Python context.
            # Assuming 'Quant' is not zero.
            if estrutura_lcl.Options[0].Quant != 0:
                estrutura_lcl.Options[0].ValorIn += (self_valor_in - estrutura_lcl_valor_in) / estrutura_lcl.Options[0].Quant

        lista_strikes: List[float] = []
        for option in estrutura_lcl.Options:
            if option.Tipo != 'ativo':
                lista_strikes.append(option.strike)
        lista_strikes.extend([minv, maxv])
        lista_strikes.sort()

        self.Plotar_Strikes_ValorAtivo_Sigma(Chart, plotarStrikes, plotarStrikes, plotarStrikes)

        for i in range(101):
            lista_strikes.append(minv + i * (maxv - minv) / 100)
        lista_strikes.sort()

        serie = Chart.criarSerie('Then')
        serie.VertAxis = "aleftaxis"
        for strike in lista_strikes:
            serie.AddXY(strike, estrutura_lcl.CalcPayoffnoVencimento(strike), '', "clTeeColor")

        estrutura_lcl.Destroy()

##    def CalcValorIn(self) -> float:
##        self.FixValorIn()
##        total = 0.0
##        for option in self.Options:
##            total += option.ValorIn * option.Quant
##        if abs(total) < 0.01:
##            total = 0.01
##        self.ValorIn = total
##        return total


    # No Estrutura.py -> Classe TEstrutura

    def CalcValorIn(self) -> float:
        self.FixValorIn()
        total = 0.0
        for option in self.Options:
            total += option.ValorIn * option.Quant

        #qb = max(1, abs(self.QuantBase))
        qb = 1
        # Salva o resultado como UNITÁRIO
        self.ValorIn = total / qb
        return self.ValorIn


#----------------------------------------------------------------------------------------------------------------
    def AddOption(self, Quant: int, Tipo: str, Oper: str, strikeN: float, Vencimento: int):
        option = TOption()
        option.Quant = Quant
        option.Tipo = Tipo
        option.Oper = Oper
        option.Serie = Vencimento # Assuming 'Serie' is equivalent to 'Vencimento' in this context
        option.strikeN = strikeN
        option.strike = 0.0 # Will be updated later if strikeN is used for strike
        option.Vencimento = Vencimento
        self.Options.Add(option)

        if Vencimento > self.MaiorVencimento:
            self.MaiorVencimento = Vencimento

#----------------------------------------------------------------------------------------------------------------
    def AddOption_with_name_and_value(self, Oper: str, Nome: str, Tipo: str, strike: float, Quant: int, ValorIn: float):
        option = TOption()
        option.Quant = Quant
        option.Nome = Nome
        option.Tipo = Tipo
        option.Oper = Oper
        option.strike = strike
        option.ValorIn = ValorIn
        self.Options.Add(option)

    # Pythonic approach to handle two AddOption signatures:
    # This example shows how to combine them or keep them separate with distinct names.
    # For a direct conversion, the original code had distinct methods that shared the same name.
    # In Python, we might prefer one comprehensive method or separate, clearly named ones.
    # For now, let's keep them distinct as in the Delphi original.

    # --- Payoff Calculation methods ---

#----------------------------------------------------------------------------------------------------------------
    def CalcPayoffnoVencimento(self, PrecoAtivo: float, CDI: Optional[float] = None) -> float:
        total = self.ValorOffset

        # Get maturities to determine if it's a single or multiple expiry structure
        maior_venc, menor_venc = self.GetVencimentos()
        is_single_maturity = (menor_venc == float('inf')) or (maior_venc == menor_venc)

        for option in self.Options:
            if option.Tipo == 'ativo':
                total += option.Quant * (PrecoAtivo - option.ValorIn)
            else: # It's an option (call or put)
                if CDI is None or is_single_maturity or (option.Vencimento == menor_venc):
                    # Intrinsic value if CDI is not provided (final maturity)
                    # or if single maturity structure, or if option is at first maturity
                    if option.Tipo == 'call':
                        total += option.Quant * CallPayoff(PrecoAtivo, option.strike, option.ValorIn)
                    elif option.Tipo == 'put':
                        total += option.Quant * PutPayoff(PrecoAtivo, option.strike, option.ValorIn)
                else: # Option with future maturity in a multi-maturity structure, use BS
                    # Reference date for BS is the first maturity date.
                    # Time to maturity for the future option is the difference.
                    dias_restantes = option.Vencimento - menor_venc
                    total += option.BlackScholesSimple(PrecoAtivo, dias_restantes, CDI, option.Volatilidade)

        return total

#----------------------------------------------------------------------------------------------------------------
    def CalcPayoffAntecipado(self, PrecoAtivo: float, diasAntesDoPrimeiroVenc: int, CDI: float) -> float:
        menor_venc = self.GetMenorVencimento()
        if menor_venc == 0:
            return self.CalcPayoffnoVencimento(PrecoAtivo) # No options, just active

        diasParaAvaliacao = menor_venc - diasAntesDoPrimeiroVenc

        if diasParaAvaliacao < 0:
            print("Error: Evaluation date has passed.")
            return 0.0

        total = self.ValorOffset
        for option in self.Options:
            if option.Tipo == 'ativo':
                total += option.Quant * (PrecoAtivo - option.ValorIn)
            else:
                dias_restantes = option.Vencimento - diasParaAvaliacao
                valor_opcao = self._PrecificarOpcaoBS(option, PrecoAtivo, dias_restantes, CDI)
                total += option.Quant * (valor_opcao - option.ValorIn)
        return total

    # --- Expected Profit Calculation methods ---

#----------------------------------------------------------------------------------------------------------------
    def CalcExpectedProfitNoVencimento(self, Sigma: float, mean: float, CDI: float = 0.0,
                                       Chart: Optional[TChart] = None, PlotExpectedProfit: bool = True,
                                       PlotProfit: bool = True, PlotSigmas: bool = True, PlotFDP: bool = True,
                                       PlotAtivo: bool = True) -> float:
        maior_venc, menor_venc = self.GetVencimentos()

        if (menor_venc != float('inf')) and (maior_venc != menor_venc) and (CDI == 0):
            print('A estrutura possui múltiplos vencimentos. Para uma análise correta, utilize a versão da função que inclui o parâmetro CDI.')
            return 0.0

        dias_para_venc = self.GetMenorVencimento()
        return self._ExecutarAnaliseLucroEsperado(Sigma, mean, dias_para_venc, CDI,
                                                  TTipoAnalise.taNoPrimeiroVencimento, 0, Chart,
                                                  PlotExpectedProfit, PlotProfit, PlotSigmas, PlotFDP, PlotAtivo, -1, -1)

#----------------------------------------------------------------------------------------------------------------
    def CalcExpectedProfitNoVencimentoComLimites(self, Sigma: float, mean: float, CDI: float, Chart: Optional[TChart],
                                                 PlotExpectedProfit: bool, PlotProfit: bool, PlotSigmas: bool,
                                                 PlotFDP: bool, PlotAtivo: bool, limiteMin: float, limiteMax: float) -> float:
        dias_para_venc = self.GetMenorVencimento()
        return self._ExecutarAnaliseLucroEsperado(Sigma, mean, dias_para_venc, CDI, TTipoAnalise.taNoPrimeiroVencimento,
                                                  0, Chart, PlotExpectedProfit, PlotProfit, PlotSigmas, PlotFDP, PlotAtivo,
                                                  limiteMin, limiteMax)

#----------------------------------------------------------------------------------------------------------------
    def CalcExpectedProfitAntecipado(self, Sigma: float, mean: float, diasAntesDoPrimeiroVenc: int, CDI: float,
                                     Chart: Optional[TChart] = None, PlotExpectedProfit: bool = True,
                                     PlotProfit: bool = True, PlotSigmas: bool = True, PlotFDP: bool = True,
                                     PlotAtivo: bool = True, limiteMin: float = -1, limiteMax: float = -1) -> float:
        menor_venc = self.GetMenorVencimento()
        if (menor_venc == 0) or (menor_venc == float('inf')):
            return 0.0 # Cannot calculate for a structure without options

        diasParaAvaliacao = menor_venc - diasAntesDoPrimeiroVenc

        return self._ExecutarAnaliseLucroEsperado(Sigma, mean, diasParaAvaliacao, CDI, TTipoAnalise.taAntecipada,
                                                  diasAntesDoPrimeiroVenc, Chart, PlotExpectedProfit, PlotProfit,
                                                  PlotSigmas, PlotFDP, PlotAtivo, limiteMin, limiteMax)

    # --- Published Methods (Pythonic way to expose) ---
#----------------------------------------------------------------------------------------------------------------
    def CalcGraphLimits(self, minv: float, maxv: float, N: float) -> Tuple[float, float]:
        self.FixValorIn()

        lista_strikes: List[float] = []
        for option in self.Options:
            if option.Tipo != 'ativo':
                lista_strikes.append(option.strike)

        lista_strikes.append(self.ValorAtivoIn)

        if minv > 0.01:
            lista_strikes.append(minv)
        if maxv > 0.01:
            lista_strikes.append(maxv)

        # Assuming calcSigma is a global function or helper
        lista_strikes.append(calcSigma(self.ValorAtivoIn, self.Sigma, round(N), -3))
        lista_strikes.append(calcSigma(self.ValorAtivoIn, self.Sigma, round(N), 3))

        lista_strikes.sort()

        minv_result = lista_strikes[0]
        maxv_result = lista_strikes[-1]

        maxv_result = maxv_result + minv_result * 0.1
        minv_result = minv_result * 0.9

        return minv_result, maxv_result

#----------------------------------------------------------------------------------------------------------------
    def CopyEstruturaTo(self, EstruturaLcl: 'TEstrutura'):
        # Campos financeiros de valor total
        EstruturaLcl.ValorIn = self.ValorIn
        EstruturaLcl.ValorAim = self.ValorAim
        EstruturaLcl.ValorOut = self.ValorOut
        EstruturaLcl.ValorLucro = self.ValorLucro
        EstruturaLcl.ValorOffset = self.ValorOffset
        EstruturaLcl.ValorNow = self.ValorNow
        # NOVAS PROPRIEDADES (Financeiras)
        EstruturaLcl.LucroAim = self.LucroAim
        EstruturaLcl.LucroAimPercentual = self.LucroAimPercentual
        EstruturaLcl.LucroOut = self.LucroOut
        # Metadados e Ativo
        EstruturaLcl.ValorAtivoIn = self.ValorAtivoIn or 0.0
        EstruturaLcl.ValorAtivoNow = self.ValorAtivoNow or 0.0
        EstruturaLcl.Nome = self.Nome
        EstruturaLcl.AtivoBase = self.AtivoBase
        EstruturaLcl.Sigma = self.Sigma
        EstruturaLcl.QuantBase = self.QuantBase
        EstruturaLcl.id = self.id
        EstruturaLcl.DTE = self.DTE
        EstruturaLcl.MenorVencimento = self.MenorVencimento
        EstruturaLcl.MaiorVencimento = self.MaiorVencimento
        EstruturaLcl.Categoria = self.Categoria
        EstruturaLcl.Corretora = self.Corretora

        EstruturaLcl.Options.Clear()
        for option in self.Options:
            new_option = TOption()
            option.CopyOptionTo(new_option)
            EstruturaLcl.Options.Add(new_option)

#----------------------------------------------------------------------------------------------------------------
    def GetUnitaryCopy(self) -> 'TEstrutura':
        """
        Retorna uma cópia da estrutura com as quantidades normalizadas para a unidade.
        Útil para plotar o gráfico de 'Lucro por Ação' ou 'Perfil da Estratégia'.
        """
        # 1. Cria uma nova instância vazia
        est_unit = TEstrutura()
        self.CopyEstruturaTo(est_unit) # Primeiro fazemos a cópia exata

        # O divisor será a QuantBase (ex: 250).
        # Se for zero ou negativa, usamos o valor absoluto ou 1 para não dar erro.
        divisor = abs(self.QuantBase) if self.QuantBase != 0 else 1

        # Ajustamos os valores totais para valores por unidade
        est_unit.ValorIn = self.ValorIn / divisor
        est_unit.ValorAim = self.ValorAim / divisor
        est_unit.ValorOut = self.ValorOut / divisor
        est_unit.ValorLucro = self.ValorLucro / divisor
        est_unit.ValorOffset = self.ValorOffset / divisor
        est_unit.ValorNow = self.ValorNow / divisor

        # Ajustamos as quantidades das opções (ex: -250 vira -1)
        for option in est_unit.Options:
            option.Quant = option.Quant / divisor
            # O ValorIn da opção individual NÃO muda (pois ele já é unitário)

        est_unit.Nome = f"{self.Nome} (Unitário)"
        return est_unit

#----------------------------------------------------------------------------------------------------------------
    def BlackScholesSimple(self, ValorAtivo: float, dias: int, ratePercAnual: float) -> float:
        total = self.ValorOffset
        for option in self.Options:
            if option.Tipo == 'ativo':
                total += option.Quant * (ValorAtivo - option.ValorIn)
            elif option.Tipo in ('call', 'put'):
                call_put_flag = 'C' if option.Tipo == 'call' else 'P'
                X = option.strike
                # Combine 'dias' from function arg with option's own 'Vencimento'
                total_dias = dias + option.Vencimento
                if total_dias <= 0:
                    total_dias = 1e-10
                time_in_years = total_dias / DIAS_UTEIS_NO_ANO

                option_value = BlackScholesOption(call_put_flag, ValorAtivo, X, time_in_years, ratePercAnual, option.Volatilidade)
                total += option.Quant * (option_value - option.ValorIn)
        return total

#----------------------------------------------------------------------------------------------------------------
    def BlackScholesSerie(self, S: float, r: float, v: float, Vencimentos: int) -> float:
        total = 0.0
        for option in self.Options:
            if option.Tipo in ('call', 'put'):
                call_put_flag = 'C' if option.Tipo == 'call' else 'P'
                X = option.strike
                # Assuming 'Vencimentos' in this context refers to a multiplier for option.Serie
                # and '21' days per month/period, and 260 trading days per year for this specific function.
                dias = 21 * (option.Serie + Vencimentos - 1) + 1e-4

                option_value = BlackScholesOption(call_put_flag, S, X, dias / 260, r, v)
                total += option.Quant * option_value
        return total

#----------------------------------------------------------------------------------------------------------------
    def GetMaiorVencimento(self) -> int:
        self.MaiorVencimento = 0
        for option in self.Options:
            if option.Vencimento > self.MaiorVencimento:
                self.MaiorVencimento = option.Vencimento
        return self.MaiorVencimento

#----------------------------------------------------------------------------------------------------------------
    def GetMenorVencimento(self) -> int:
        result = float('inf') # Use float('inf') for initial high value
        for option in self.Options:
            if option.Tipo != 'ativo':
                if option.Vencimento < result:
                    if option.Status in [TOptionStatus.ATIVA]:
                        result = option.Vencimento
        return int(result) if result != float('inf') else 0

#----------------------------------------------------------------------------------------------------------------
    def GetVencimentos(self) -> Tuple[int, int]:
        # This function seems to be used to populate a dummy TArray<Integer> in Delphi,
        # but the actual interesting part is returning MaiorVencimento and menorVencimento.
        maior_vencimento = 0
        menor_vencimento = float('inf')
        for option in self.Options:
            if option.Tipo != 'ativo':
                if option.Vencimento > maior_vencimento:
                    maior_vencimento = option.Vencimento
                if option.Vencimento < menor_vencimento:
                    menor_vencimento = option.Vencimento
        return maior_vencimento, int(menor_vencimento) if menor_vencimento != float('inf') else 0

#----------------------------------------------------------------------------------------------------------------
    def GetXLimits(self, minX_ref: float, maxX_ref: float) -> Tuple[float, float]:
        minX = float('inf')
        maxX = float('-inf')
        for option in self.Options:
            if option.Quant != 0:
                if option.strike != 0:
                    minX = min(minX, option.strike)
                    maxX = max(maxX, option.strike)
                if option.Tipo == 'ativo':
                    minX = min(minX, option.ValorIn)
                    maxX = max(maxX, option.ValorIn)

        # Merge with provided references if any
        if minX_ref != 0 and minX_ref < minX: minX = minX_ref
        if maxX_ref != 0 and maxX_ref > maxX: maxX = maxX_ref

        # Handle case where no options or strikes were found
        if minX == float('inf'): minX = 0.0
        if maxX == float('-inf'): maxX = 0.0

        return minX, maxX


#----------------------------------------------------------------------------------------------------------------
    def GetSymbolList(self) -> str:
        return "".join(option.Nome for option in self.Options)

#----------------------------------------------------------------------------------------------------------------
    def CalcExpectedProfitBSPonderado(self, Sigma: float, mean: float, N: int, MaiorVencimento: int,
                                      menorVencimento: int, CDI: float, GanhoMin: float, Chart: Optional[TChart]) -> float:
        plotGraph = (Chart is not None)

        VolatilityForNDays = Sigma * math.sqrt(N)
        volatility = VolatilityForNDays * mean

        if plotGraph:
            Chart.ClearChart()
            Chart.View3D = False
            # Chart.Legend.CustomPosition = True # No direct equivalent

            serie = Chart.criarSerie('F.D.P.', 5, "psNothing", False)
            serieLine = Chart.criarSerie('Profit', 5, "psNothing", False)
            serieHope = Chart.criarSerie('Expected profit', 5, "psNothing", False)

        Xm = mean
        a = 3 * volatility
        b = GanhoMin

        normalTotal = 0.0
        X = mean - 3 * volatility
        step = volatility / 25
        hopeTotal = 0.0

        while X < min(mean + 3 * volatility, 120.0):
            FDPValue = NormalCurve(X, mean, volatility)
            normalTotal += FDPValue

            if X > 0:
                PayoffValue = self.CalcPayoffnoVencimento(X, CDI)
                ajustePayoff = (1 - b) * math.exp(-3 * ((X - Xm) / a)**2) + b
                hope = FDPValue * PayoffValue * ajustePayoff
                hopeTotal += hope

                if plotGraph:
                    serie.AddXY(X, FDPValue, '', "clTeeColor")
                    serieLine.AddXY(X, PayoffValue, '', "clTeeColor")
                    serieHope.AddXY(X, hope, '', "clTeeColor")
            X += step

        if plotGraph:
            serie = Chart.criarSerie('Sigmas', 3, "pscircle", False)
            serie.Pen.Visible = False
            for i in range(7):
                serie.AddXY(mean + (i - 3) * volatility, 0, '', "clTeeColor")

            serie = Chart.criarSerie('Ativo', 5, "psrectangle", False)
            serie.Pen.Visible = False
            serie.AddXY(mean, 0, '', "clTeeColor")

        return hopeTotal * step


#----------------------------------------------------------------------------------------------------------------
    def FazerEstruturaAgressiva(self, agressividade: float) -> 'TEstrutura':
        # Result is passed as var, meaning it's modified in place or a new object is returned and assigned.
        # Python doesn't have `var` parameters, so we'll create and return a new instance.
        result_estrutura = TEstrutura()
        self.CopyEstruturaTo(result_estrutura)

        for option in result_estrutura.Options:
            if option.ValorLast == 0:
                # Assuming 'spreadTotal' here is meant to be 0 or some other calculated value,
                # as it was initially not explicitly set inside this loop in Delphi.
                # If it's a fixed value, define it. If it's 0, it won't change ask/bid.
                spread_total = 0 # This needs clarification if it's meant to be something else.
                option.ValorAsk -= spread_total
                option.ValorBid += spread_total
            else:
                option.ValorAsk = option.ValorLast
                option.ValorBid = option.ValorLast
        result_estrutura.ValorIn = result_estrutura.CalcValorIn()
        return result_estrutura

    def FazerEstruturaAgressivaSpread(self, agressividade: float) -> 'TEstrutura':
        result_estrutura = TEstrutura()
        self.CopyEstruturaTo(result_estrutura)

        custo_total = 0.0
        for i, option in enumerate(self.Options):
            option.Valorspread = option.ValorAsk - option.ValorBid
            spread_total = roundto(option.Valorspread * agressividade, -2) # Round to 2 decimal places

            sinal_oper = sign(result_estrutura.Options[i].Quant)
            result_estrutura.Options[i].ValorAsk -= spread_total
            result_estrutura.Options[i].ValorBid += spread_total
            result_estrutura.Options[i].ValorIn = result_estrutura.Options[i].ValorIn - sinal_oper * spread_total
            custo_total += result_estrutura.Options[i].Quant * result_estrutura.Options[i].ValorIn

        result_estrutura.ValorIn = custo_total
        return result_estrutura

#----------------------------------------------------------------------------------------------------------------
    def EstruturaToGrid(self) -> pd.DataFrame:
        """
        Substitui TStringGrid por pandas.DataFrame para estruturar os dados da estrutura.
        Retorna um DataFrame que pode ser facilmente exportado para HTML.
        """
        # Cabeçalho original: 'Quant,Tipo,Oper,Nome,Strike,ValorIn'
        dados = []

        self.FixValorIn()

        for option in self.Options:
            # Criando o formato (Valor, PayOff, %) - Os últimos 5 campos não são mais relevantes
            # para a TStringGrid padrão, mas eram usados para botões na tela (←, →, -1, etc.)
            # Vamos nos concentrar apenas nos dados das opções.
            dados.append({
                'Quant': option.Quant,
                'Tipo': option.Tipo,
                'Oper': option.Oper,
                'Nome': option.Nome,
                'Strike': option.strike,
                'Valor In': option.ValorIn
            })

        df = pd.DataFrame(dados)

        # Adicionando as colunas extras que eram usadas para botões/referência, se necessário.
        # Se o objetivo é HTML, é melhor omitir estes campos ou colocá-los no template.
        # Exemplo: df['←'] = '←', etc. Vamos manter limpo.

        # Formatação (opcional, para visualização)
        df['Strike'] = df['Strike'].map('{:.2f}'.format)
        df['Valor In'] = df['Valor In'].map('{:.2f}'.format)

        return df

#----------------------------------------------------------------------------------------------------------------
    def EstruturaToGrid_Detalhes(self, Grid: TStringGrid):
        try:
            Grid.RowCount = len(self.Options) + 1
            Grid.FixedCols = 0
            Grid.ColCount = 10

            Grid.rows[0].CommaText = 'Quant,tipo,Oper,Nome,Strike,ValorIn,Dias,Volat,Vol_Imp'

            self.FixValorIn()

            for i, option in enumerate(self.Options):
                Grid.Cells(0, i + 1).value = str(option.Quant)
                Grid.Cells(1, i + 1).value = option.Tipo
                Grid.Cells(2, i + 1).value = option.Oper
                Grid.Cells(3, i + 1).value = option.Nome
                Grid.Cells(4, i + 1).value = f"{option.strike:.2f}"
                Grid.Cells(5, i + 1).value = f"{option.ValorIn:.2f}"
                Grid.Cells(6, i + 1).value = str(option.Vencimento)
                Grid.Cells(7, i + 1).value = f"{option.Volatilidade:.4f}"
                Grid.Cells(8, i + 1).value = ''
                Grid.Cells(9, i + 1).value = ''

            Grid.AutoSizeColumns()
        finally:
            pass

#----------------------------------------------------------------------------------------------------------------
    def FixValorIn(self):
        for option in self.Options:
            if option.Quant > 0:
                option.ValorIn = option.ValorAsk
            else:
                option.ValorIn = option.ValorBid

#----------------------------------------------------------------------------------------------------------------
    def NormalizarQuants(self):
        def mdc(a: int, b: int) -> int:
            while b:
                a, b = b, a % b
            return a

        def mdc_arr(valores: List[int]) -> int:
            if not valores: return 1
            result = valores[0]
            for i in range(1, len(valores)):
                result = mdc(result, valores[i])
            return abs(result) # ensure positive MDC

        valores_quant = [option.Quant for option in self.Options]

        if not valores_quant:
            return # No options to normalize

        mdc_value = mdc_arr(valores_quant)

        if mdc_value == 0: # Avoid division by zero if all quants are zero
            return

        for option in self.Options:
            option.Quant = round(option.Quant / mdc_value)

#----------------------------------------------------------------------------------------------------------------
    def PayOffsToGridBS(self, minv: float, maxv: float, CDI: float) -> pd.DataFrame:
        """
        Substitui TStringGrid por pandas.DataFrame para estruturar os dados de Payoff.
        Retorna um DataFrame que pode ser facilmente exportado para HTML.
        """
        lista_strikes: List[float] = []
        for option in self.Options:
            if option.Tipo != 'ativo':
                if option.strike not in lista_strikes:
                    lista_strikes.append(option.strike)

        # Adiciona os limites do gráfico para garantir a tabela cobre a faixa
        lista_strikes.extend([minv, maxv])
        lista_strikes.sort()

        # Adiciona pontos intermediários para uma curva suave (opcional, mas bom para HTML)
        num_pontos = 50
        for i in range(1, num_pontos):
            preco = minv + i * (maxv - minv) / num_pontos
            if preco not in lista_strikes:
                lista_strikes.append(preco)
        lista_strikes.sort()

        dados = []

        # Título original: 'Valor,PayOff,%'
        for strike_val in lista_strikes:
            payoff = self.CalcPayoffnoVencimento(strike_val, CDI)

            percentage = 0.0
            if self.ValorIn != 0:
                percentage = (payoff * 100) / self.ValorIn

            dados.append({
                'Valor Ativo (R$)': strike_val,
                'PayOff (R$)': payoff,
                'Retorno (%)': percentage
            })

        df = pd.DataFrame(dados)

        # Formatação
        df['Valor Ativo (R$)'] = df['Valor Ativo (R$)'].map('{:.2f}'.format)
        df['PayOff (R$)'] = df['PayOff (R$)'].map('{:.2f}'.format)
        df['Retorno (%)'] = df['Retorno (%)'].map('{:.2f}'.format)

        return df


#----------------------------------------------------------------------------------------------------------------
    def Plotar_Strikes_ValorAtivo_Sigma_Matplotlib_SVG(self, ax: Any, plotarStrikes: bool, plotarValorAtivo: bool, plotarSigmas: bool):
        """
        Plota Strikes, Preço do Ativo e Sigmas no objeto Axes do Matplotlib.
        Mantendo as cores e estilos da especificação original.
        """
        import numpy as np
        import matplotlib.pyplot as plt
        import matplotlib.colors as mcolors

        # Constante de Dias Úteis (se não estiver definida globalmente)
        DIAS_UTEIS_NO_ANO = 251

        # Recalcula os limites Y antes de posicionar o texto
        x_min_limit, x_max_limit = ax.get_xlim()
        y_min_limit, y_max_limit = ax.get_ylim()

        # 1. Plotar Strikes (Cor: orangered)
        if plotarStrikes:
            # Revalidar limites Y após plotagem do payoff
            #y_min, y_max = ax.get_ylim()

            strikes_calls = [op.strike for op in self.Options if op.Tipo == 'call']
            strikes_puts = [op.strike for op in self.Options if op.Tipo == 'put']

            # Cor para os strikes
            strike_color = mcolors.CSS4_COLORS['orangered']

            # Strikes - Calls (marker: >)
            for strike in strikes_calls:
                ax.axvline(strike, color=strike_color, linestyle=':', linewidth=1, alpha=0.7)
                # Adiciona marker (simulando Triângulo para a direita)
                ax.plot(strike, 0, marker='>', color=strike_color, markersize=7, zorder=5,
                           markeredgecolor='black', markeredgewidth=0.5)

            # Strikes - Puts (marker: <)
            for strike in strikes_puts:
                ax.axvline(strike, color=strike_color, linestyle=':', linewidth=1, alpha=0.7)
                # Adiciona marker (simulando Triângulo para a esquerda)
                ax.plot(strike, 0, marker='<', color=strike_color, markersize=7, zorder=5,
                           markeredgecolor='black', markeredgewidth=0.5)

        # 2. Plotar Desvios Padrão (Sigmas) (Cor: orangered, Linha: tracejada)
        if plotarSigmas and self.Sigma > 0 and self.ValorAtivoIn > 0:

            maior_venc, menor_venc = self.GetVencimentos()
            dias_projecao = menor_venc if menor_venc > 0 else 21

            # Cálculo da Volatilidade em R$
            volatility_reais = self.ValorAtivoIn * self.Sigma * np.sqrt(dias_projecao)# / DIAS_UTEIS_NO_ANO)

            if volatility_reais > 0:
                # Posição Y para os rótulos de Sigma (0.5 na metade do y_min_limit, para ficar no quadrante inferior)
                # Usando um valor absoluto para evitar problemas se y_min_limit for zero ou positivo
                text_y_pos = y_min_limit + (y_max_limit - y_min_limit) * 0.10 # 10% do fundo para cima

                for i in range(-3, 4): # -3 Sigma a +3 Sigma
                    if i == 0: continue

                    sigma_val = self.ValorAtivoIn + i * volatility_reais

                    # Linha vertical tracejada com cor 'orangered'
                    ax.axvline(sigma_val, color=mcolors.CSS4_COLORS['orangered'], linestyle='--', linewidth=1, alpha=0.6)

                    # Adiciona texto para identificação
                    if x_min_limit <= sigma_val <= x_max_limit:
                        ax.text(
                            sigma_val,
                            text_y_pos,
                            f'{i}σ',
                            verticalalignment='center',
                            horizontalalignment='center',
                            fontsize=10,
                            color=mcolors.CSS4_COLORS['orangered'], # Cor do texto 'orangered'
                            bbox={
                                'facecolor': 'white', # Fundo branco (simulando caixa branca)
                                'alpha': 0.8,
                                'edgecolor': 'none', # Sem borda
                                'boxstyle': 'round,pad=0.2'
                            },
                            zorder=10
                        )

        # --- NOVO: Linha de Valor Alvo (ValorAim) ---
        if hasattr(self, 'ValorAim') and self.ValorAim != 0:
            y_alvo = self.ValorAim - self.ValorIn
            ax.axhline(y=y_alvo, color='purple', linestyle='--', linewidth=1.5, alpha=0.8)

            # MUDANÇA AQUI: Usamos transform=ax.get_yaxis_transform() para fixar o texto na esquerda
            # sem alterar os limites do gráfico. x=0.01 significa 1% da largura do quadro.
            ax.text(0.01, y_alvo, f' Alvo: {y_alvo:.2f}', color='purple',
                    va='bottom', fontsize=9, fontweight='bold',
                    transform=ax.get_yaxis_transform())
            print('\n\nPlotar_Strikes_ValorAtivo_Sigma_Matplotlib_SVG: y_alvo: ValorAim', self.ValorAim, '   valorin:', self.ValorIn,'\n\n')

        # --- NOVO: Círculo no Valor Inicial do Ativo e no Zero (Ponto de Entrada) ---
        if self.ValorAtivoIn > 0:
            ax.plot(self.ValorAtivoIn, 0, marker='o', color='black', markersize=10,
                    fillstyle='none', markeredgewidth=2, label='Entrada', zorder=20)

        # --- NOVO: Quadrado no Valor Atual do Ativo e P&L Atual (Mark-to-Market) ---
        # Determinamos o X atual (ValorAtivoNow ou fallback para ValorAtivo)
        x_atual = self.ValorAtivoNow if self.ValorAtivoNow > 0 else self.ValorAtivoIn
        # O Y é o ValorNow da estrutura (que calculamos no server.py como PayoffAntecipado)
        y_atual = (self.ValorNow/self.QuantBase - self.ValorIn)
        print('\n\nPlotar_Strikes_ValorAtivo_Sigma_Matplotlib_SVG: valorNow:\n', self.ValorNow, '   valorin:', self.ValorIn,'\n\n')

        if x_atual > 0:
            ax.plot(x_atual, y_atual, marker='s', color='blue', markersize=10,
                    label='P&L Atual', zorder=21)

            # Adiciona uma linha conectando o preço atual ao eixo X para facilitar leitura
            ax.vlines(x_atual, min(0, y_atual), max(0, y_atual), color='blue', linestyle=':', alpha=0.5)

        # 3. Plotar Valor Ativo (Preço Spot) - (Mantido o código original)
        if plotarValorAtivo and self.ValorAtivoIn > 0:
            ax.axvline(
                self.ValorAtivoIn,
                color='green',
                linestyle=':',
                linewidth=2.0,
                label=f'Preço Entrada ({self.ValorAtivoIn:.2f})',
                zorder=4
            )

#----------------------------------------------------------------------------------------------------------------
    def Plotar_Strikes_ValorAtivo_Sigma(self, ax: Any, plotarStrikes: bool, plotarValorAtivo: bool, plotarSigmas: bool):
        """
        Plota Strikes, Preço do Ativo e Sigmas no objeto Axes do Matplotlib.
        Substitui TChart por ax (matplotlib.axes.Axes).
        """
        import numpy as np
        import matplotlib.pyplot as plt
        import matplotlib.colors as mcolors
        # Assumindo que math está importado no módulo

        y_min, y_max = ax.get_ylim()

        # 1. Plotar Strikes (Substituindo criarSerie/AddXY)
        if plotarStrikes:
            strikes_calls = [op.strike for op in self.Options if op.Tipo == 'call']
            strikes_puts = [op.strike for op in self.Options if op.Tipo == 'put']

            # Matplotlib não tem Triângulos Simples no eixo X, usamos ax.axvline e texto/markers

            # Strikes - Calls
            for strike in strikes_calls:
                ax.axvline(strike, color=mcolors.CSS4_COLORS['orangered'], linestyle=':', linewidth=1, alpha=0.7)
                # Adiciona marker (simulando Triângulo)
                ax.plot(strike, 0, marker='>', color=mcolors.CSS4_COLORS['orangered'], markersize=7, zorder=5,
                           markeredgecolor='black', markeredgewidth=0.5)

            # Strikes - Puts
            for strike in strikes_puts:
                ax.axvline(strike, color=mcolors.CSS4_COLORS['orangered'], linestyle=':', linewidth=1, alpha=0.7)
                # Adiciona marker (simulando Triângulo)
                ax.plot(strike, 0, marker='<', color=mcolors.CSS4_COLORS['orangered'], markersize=7, zorder=5,
                           markeredgecolor='black', markeredgewidth=0.5)

        # 2. Plotar Desvios Padrão (Sigmas)
        if plotarSigmas and self.Sigma > 0 and self.ValorAtivoIn > 0:
            #DIAS_UTEIS_NO_ANO = 251 # Usando constante
            maior_venc, menor_venc = self.GetVencimentos()
            dias_projecao = menor_venc if menor_venc > 0 else 21

            volatility_for_n_days = self.Sigma * np.sqrt(dias_projecao)# / DIAS_UTEIS_NO_ANO)
            volatility_reais = volatility_for_n_days * self.ValorAtivoIn # Volatilidade em R$

            if volatility_reais > 0:
                for i in range(-3, 4): # -3 Sigma a +3 Sigma
                    sigma_val = self.ValorAtivoIn + i * volatility_reais
                    ax.axvline(sigma_val, color=mcolors.CSS4_COLORS['orangered'], linestyle='--', linewidth=1, alpha=0.6)
                    # Adiciona texto para identificação
                    #ax.text(sigma_val, y_min * 0.5, f'{i}σ', verticalalignment='center', fontsize=8, color=mcolors.CSS4_COLORS['orangered'],)
                    ax.text(
                        sigma_val,
                        y_min * 0.5, # Posição Y (Metade da distância de y_min)
                        f'{i}σ',
                        verticalalignment='center', # Centralizado verticalmente
                        horizontalalignment='center', # <--- CENTRALIZAR HORIZONTALMENTE
                        fontsize=10, # <--- AUMENTAR FONTE
                        color=mcolors.CSS4_COLORS['orangered'],
                        bbox={
                            'facecolor': 'white',
                            'alpha': 0.8,
                            'edgecolor': 'none', # <--- BORDA BRANCA (Simulada por fundo branco com sem borda)
                            'boxstyle': 'round,pad=0.2'
                        }
                    )


        # 3. Plotar Valor Ativo (Preço Spot)
        if plotarValorAtivo and self.ValorAtivoIn > 0:
            # Substitui o "psrectangle" por uma linha vertical sólida
            ax.axvline(self.ValorAtivoIn, color='green', linestyle='-', linewidth=2.0, label=f'Preço Spot ({self.ValorAtivoIn:.2f})')

        # Não usamos Chart.criarSerie para legendas aqui, o chamador deve usar ax.legend()
#    def Plotar_Strikes_ValorAtivo_Sigma(self, Chart: TChart, plotarStrikes: bool, plotarValorAtivo: bool, plotarSigmas: bool):
#        if plotarStrikes:
#            # plot strikes - calls
#            serie_calls = Chart.criarSerie('Calls', "psrighttriangle")
#            serie_calls.Color = "clWebDarkOrange"
#            serie_calls.ShowInLegend = False
#            for option in self.Options:
#                if option.Tipo == 'call':
#                    serie_calls.AddXY(option.strike, 0, '', "clTeeColor")
#
#            # plot strikes - puts
#            serie_puts = Chart.criarSerie('Puts', "psLeftTriangle")
#            serie_puts.Color = "clWebDarkOrange"
#            serie_puts.ShowInLegend = False
#            for option in self.Options:
#                if option.Tipo == 'put':
#                    serie_puts.AddXY(option.strike, 0, '', "clTeeColor")
#
#        # plot standard deviations
#        if plotarSigmas:
#            maior_venc, menor_venc = self.GetVencimentos()
#
#            volatility_for_n_days = 0.0
#            if menor_venc > 0:
#                volatility_for_n_days = self.Sigma * math.sqrt(menor_venc)
#            elif menor_venc == 0 and len(self.Options) > 0: # If only active in structure, use a small default for calculation
#                volatility_for_n_days = self.Sigma * math.sqrt(0.5)
#
#            volatility = volatility_for_n_days * self.ValorAtivo
#
#            if volatility > 0: # Only plot sigmas if volatility is meaningful
#                serie_sigmas = Chart.criarSerie('Sigmas', 3, "pscircle", False)
#                serie_sigmas.Pen.Visible = False
#                serie_sigmas.ShowInLegend = False
#                serie_sigmas.Color = "clRed"
#                for i in range(7): # -3 to +3 sigmas
#                    serie_sigmas.AddXY(self.ValorAtivo + 1 * (i - 3) * volatility, 0, '', "clTeeColor")
#
#        if plotarValorAtivo:
#            if self.ValorAtivo > 0:
#                serie_ativo = Chart.criarSerie('Ativo', "psrectangle")
#                serie_ativo.Color = "clRed"
#                serie_ativo.ShowInLegend = False
#                serie_ativo.AddXY(self.ValorAtivo, 0, '', "clTeeColor")




#----------------------------------------------------------------------------------------------------------------
    def SetarEstrutura(self, tipos: List[str], quants: List[int]):
        # This method assumes that 'tipos' and 'quants' are aligned
        # and that the remaining option details (strike, oper, etc.) will be filled later.
        if len(tipos) != len(quants):
            raise ValueError("Length of 'tipos' and 'quants' arrays must be equal.")

        for i in range(len(tipos)):
            c = 'C' if quants[i] > 0 else 'V' # Buy (C) or Sell (V) based on quantity
            # Assuming AddOption with 4 args is the intent
            self.AddOption(quants[i], tipos[i], c, 0.0, 0) # StrikeN and Vencimento set to 0 initially

#----------------------------------------------------------------------------------------------------------------
    def CotarEstrutura(self, PreencherValorIn: bool, PreencherValorOut: bool):
        valor_ativo_found = 0.0

        lista_ativos = TStringList()
        for option in self.Options:
            lista_ativos.Add(option.Nome)
        lista_ativos.Add(self.AtivoBase) # Ensure base active is included

        stock_data: List[TOplabStockData] = []
        oplab_instance = TOpLab().Create() # Assuming it needs to be instantiated each time if not a singleton.

        try:
            oplab_instance.CotarSymbolsplus(lista_ativos.CommaText, stock_data)

            total_m = 0.0 # Montagem (Cost to build/buy the structure)
            total_dm = 0.0 # Desmontagem (Value to dismantle/sell the structure)

            for j, option in enumerate(self.Options):
                for k, data in enumerate(stock_data):
                    if option.Nome == data.Symbol:
                        if option.Oper == 'C': # If we are 'buying' (Oper = C)
                            total_dm += option.Quant * data.bid # Value if we sell (bid)
                            total_m += option.Quant * data.ask # Cost if we buy (ask)
                            if PreencherValorIn:
                                option.ValorIn = data.ask # Set ValorIn for options being bought
                            if PreencherValorOut:
                                option.ValorOut = data.bid # Set ValorOut for options being sold
                        else: # If we are 'selling' (Oper = V)
                            total_dm += option.Quant * data.ask # Value if we buy back (ask)
                            total_m += option.Quant * data.bid # Cost if we sell (bid)
                            if PreencherValorIn:
                                option.ValorIn = data.bid # Set ValorIn for options being sold (initially)
                            if PreencherValorOut:
                                option.ValorOut = data.ask # Set ValorOut for options being bought back (initially)

                        option.ValorBid = data.bid
                        option.ValorAsk = data.ask
                        break # Found symbol, move to next option

            # Update ValorAtivo from stock_data
            for data in stock_data:
                if self.AtivoBase == data.Symbol:
                    valor_ativo_found = data.bid # Assuming bid price for ValorAtivo
                    break
            self.ValorAtivoIn = valor_ativo_found

            if PreencherValorOut:
                self.ValorOut = total_dm
            if PreencherValorIn:
                self.ValorIn = total_m

        finally:
            for data in stock_data:
                data.Destroy()
            lista_ativos = None # Python handles GC, no explicit destroy needed usually, but following Delphi pattern
            oplab_instance.Destroy()

#----------------------------------------------------------------------------------------------------------------
    def DerivarEstruturaParaVencimentoFuturo(self, VencimentoReferencia: int) -> 'TEstrutura':
        estrutura_futura = TEstrutura()

        estrutura_futura.ValorAtivoIn = self.ValorAtivoIn
        estrutura_futura.AtivoBase = self.AtivoBase
        estrutura_futura.Sigma = self.Sigma
        estrutura_futura.Nome = self.Nome + ' (Restante)'

        for option in self.Options:
            if (option.Vencimento > VencimentoReferencia) or (option.Tipo == 'ativo'):
                new_option = TOption()
                option.CopyOptionTo(new_option)
                estrutura_futura.Options.Add(new_option)

        return estrutura_futura

#----------------------------------------------------------------------------------------------------------------
    def DerivarEstruturaComVencimentoUnico(self) -> 'TEstrutura':
        estrutura_simplificada = TEstrutura()
        self.CopyEstruturaTo(estrutura_simplificada)

        estrutura_simplificada.Nome = self.Nome + ' (Venc. Unificado)'

        maior_vencimento, menor_venc_dummy = self.GetVencimentos()

        if menor_venc_dummy == float('inf'): # No options, only active
            return estrutura_simplificada

        # Adjust all options' maturities to the final maturity
        for option in estrutura_simplificada.Options:
            if option.Tipo != 'ativo':
                option.Vencimento = maior_vencimento

        estrutura_simplificada.CalcValorIn()

        return estrutura_simplificada

#----------------------------------------------------------------------------------------------------------------
    def CalcProbabilidadeNaFaixa(self, Sigma: float, mean: float, N: int, limiteMin: float, limiteMax: float) -> float:
        volatility_for_n_days = Sigma * math.sqrt(N)
        volatility = volatility_for_n_days * mean

        X = mean - NUM_DESVIOS_PADRAO_INTEGRACAO * volatility
        step = volatility / PASSOS_POR_DESVIO_PADRAO
        prob_total = 0.0

        while X < mean + NUM_DESVIOS_PADRAO_INTEGRACAO * volatility:
            if (X >= limiteMin) and (X <= limiteMax):
                FDPValue = NormalCurve(X, mean, volatility)
                prob_total += FDPValue
            X += step
        return prob_total * step

#----------------------------------------------------------------------------------------------------------------
    def CalcExpectedProfitCondicionalDoisVencimentos(self, Sigma: float, mean: float, N1: int, N2: int,
                                                     CDI: float, Chart: Optional[TChart]) -> float:
        limite_inf_nao_exercicio = -1.0
        limite_sup_nao_exercicio = -1.0

        for option in self.Options:
            if option.Vencimento == N1:
                if option.Tipo == 'put':
                    if (limite_inf_nao_exercicio == -1) or (option.strike > limite_inf_nao_exercicio):
                        limite_inf_nao_exercicio = option.strike
                if option.Tipo == 'call':
                    if (limite_sup_nao_exercicio == -1) or (option.strike < limite_sup_nao_exercicio):
                        limite_sup_nao_exercicio = option.strike

        if (limite_inf_nao_exercicio != -1) and (limite_sup_nao_exercicio != -1) and \
           (limite_inf_nao_exercicio > limite_sup_nao_exercicio):
            limite_inf_nao_exercicio = 1.0 # These values seems like flags/sentinels in Delphi
            limite_sup_nao_exercicio = 0.0 # indicating an impossible non-exercise range.

        # Contribution from scenario 1 (EXERCISE at N1)
        E_Total_N1 = self.CalcExpectedProfitNoVencimentoComLimites(Sigma, mean, CDI, None,
                                                                    False, False, False, False, False, -1, -1)
        E_Contrib_NaoExercicio_N1 = self.CalcExpectedProfitNoVencimentoComLimites(Sigma, mean, CDI, None,
                                                                                 False, False, False, False, False,
                                                                                 limite_inf_nao_exercicio, limite_sup_nao_exercicio)
        Esperanca_Se_Exercicio_N1 = E_Total_N1 - E_Contrib_NaoExercicio_N1

        # Contribution from scenario 2 (NO EXERCISE at N1, rolling to N2)
        estrutura_remanescente: Optional[TEstrutura] = None
        Esperanca_Se_NaoExercicio_N2 = 0.0
        try:
            probabilidade_nao_exercicio = self.CalcProbabilidadeNaFaixa(Sigma, mean, N1, limite_inf_nao_exercicio, limite_sup_nao_exercicio)

            if probabilidade_nao_exercicio > 0:
                estrutura_remanescente = self.DerivarEstruturaParaVencimentoFuturo(N1)

                # Optimized call to the master execution function
                E_Total_N2 = estrutura_remanescente._ExecutarAnaliseLucroEsperado(Sigma, mean, N2,
                                                                                 CDI, TTipoAnalise.taNoPrimeiroVencimento,
                                                                                 0, None, False, False, False, False, False, -1, -1)
                Esperanca_Se_NaoExercicio_N2 = probabilidade_nao_exercicio * E_Total_N2
        finally:
            if estrutura_remanescente:
                estrutura_remanescente.Destroy()

        return Esperanca_Se_Exercicio_N1 + Esperanca_Se_NaoExercicio_N2


#----------------------------------------------------------------------------------------------------------------
    # Est: CotarEstruturaAPI
    def CotarEstruturaAPI(self):
        """
        Método de instância que cota e enriquece a própria estrutura usando a API Oplab.
        É a versão final da função 'enriquecer_estrutura'.
        """
        # Certifique-se de que TOpLab e TOplabStockData estão importados no Estrutura.py
        print('\n\nCotarEstruturaAPI:', self.txt(), '\n\n')
        oplab_instance = TOpLab().Create()
        stock_data: List[TOplabStockData] = []
        lista_ativos = []

        try:
            # 1. Obter a lista de símbolos
            for option in self.Options:
                lista_ativos.append(option.Nome)

            if self.AtivoBase and self.AtivoBase not in lista_ativos:
                 lista_ativos.append(self.AtivoBase)

            if not self.AtivoBase and self.Options:
                 base_ticker = self.Options[0].Nome[:4]
                 if base_ticker in ('BOVA', 'PETR', 'VALE'):
                     ativo_completo = base_ticker + '11' if base_ticker == 'BOVA' else base_ticker + '4'
                     lista_ativos.append(ativo_completo)

            # 2. Cotar símbolos (API REAL)
            comma_separated_symbols = ",".join(lista_ativos)
            print(f"DEBUG: Consultando API para símbolos: {comma_separated_symbols}")
            stock_data = oplab_instance.CotarSymbolsplus(comma_separated_symbols)

            # 3. Processar dados e atualizar a Estrutura (Primeira Passagem)
            sigma_encontrado = 0.0

            for option in self.Options:
                if option.Tipo == 'ativo':
                    option.strike = float('nan')
                    option.Vencimento = float('nan')

                for data in stock_data:
                    if option.Nome == data.Symbol:
                        # Só atualiza strike se não for ativo
                        if option.Tipo != 'ativo' and data.Strike > 0:
                            option.strike = data.Strike

                        # Atualiza vencimento apenas se não for ativo
                        if option.Tipo != 'ativo':
                            option.Vencimento = data.Vencimento

                        option.ValorAsk = data.Ask
                        option.ValorBid = data.Bid
                        option.ValorLast = data.Ask

                        if data.Sigma > 0:
                            sigma_encontrado = data.Sigma

                        if data.AtivoBase and len(data.AtivoBase) > 3:
                            self.AtivoBase = data.AtivoBase

                        if hasattr(data, 'ValorAtivo') and data.ValorAtivo > 0:
                            #self.ValorAtivoIn = data.ValorAtivo
                            self.ValorAtivoNow = data.ValorAtivo
                            if self.ValorAtivoIn <= 0.01:
                                self.ValorAtivoIn = data.ValorAtivo

                        break

            # 4. Processar Ativo Base e Sigma (Fallback)
            if stock_data:
                 if not self.AtivoBase and stock_data[0].AtivoBase:
                     self.AtivoBase = stock_data[0].AtivoBase
                 if not self.ValorAtivoIn and stock_data[0].ValorAtivo:
                     self.ValorAtivoIn = stock_data[0].ValorAtivo

                 if self.Sigma == 0 and sigma_encontrado > 0:
                     self.Sigma = sigma_encontrado

            # 5. Fallback Final para Sigma (Se Sigma ainda for 0, cotar apenas o AtivoBase)
            if self.Sigma == 0 and self.AtivoBase:
                print(f"DEBUG: Sigma zerado. Cotando apenas {self.AtivoBase} para Sigma.")
                stock_data_ativo = oplab_instance.CotarSymbolsplus(self.AtivoBase)

                if stock_data_ativo and stock_data_ativo[0].Sigma > 0:
                     self.Sigma = stock_data_ativo[0].Sigma

                if stock_data_ativo and stock_data_ativo[0].ValorAtivo > 0:
                     self.ValorAtivoIn = stock_data_ativo[0].ValorAtivo

                stock_data_ativo.clear()
            print('\n\nCotarEstruturaAPI:', self.txt(), '\n\n')

        finally:
            oplab_instance.Destroy()

#----------------------------------------------------------------------------------------------------------------
    def GerarDataFramePayoff(self, min_preco: float, max_preco: float, num_pontos: int = 100, cdi_anual: float = 0.0) -> pd.DataFrame:
        """
        Gera um DataFrame de Payoff (Lucro/Prejuízo) para plotagem.

        Args:
            min_preco, max_preco: Limites do eixo X para o preço do ativo.
            num_pontos: Número de pontos a serem calculados.
            cdi_anual: Taxa de juros (CDI), usado no CalcPayoffnoVencimento.

        Retorna:
            pd.DataFrame com colunas ['Preço do Ativo', 'Lucro/Prejuízo', 'Estratégia'].
        """
        import numpy as np # Importa numpy localmente

        # O Payoff é calculado na data do primeiro vencimento (ou final)
        # Usamos CalcPayoffnoVencimento que lida com vencimentos múltiplos (híbrido) ou único (intrínseco)

        precos = np.linspace(min_preco, max_preco, num_pontos)

        # Prepara a lista de Payoffs
        payoffs = []
        for preco in precos:
            # Chama o método de cálculo de payoff existente na classe
            # Passa o CDI se for necessário para o cálculo híbrido (múltiplos vencimentos)
            payoff_val = self.CalcPayoffnoVencimento(preco, cdi_anual)
            payoffs.append(payoff_val)

        # O lucro/prejuízo é o Payoff - o Custo de Montagem (ValorIn)
        lucro_prejuizo = np.array(payoffs)

        df_payoff = pd.DataFrame({
            'Preço do Ativo': precos,
            'Lucro/Prejuízo': lucro_prejuizo, # O CalcPayoff já inclui o ValorIn subtraído
            'Estratégia': self.Nome # Usa o nome da própria estrutura
        })

        return df_payoff

#----------------------------------------------------------------------------------------------------------------
    @classmethod
    def ConverterEstruturaJSONParaEstrutura(cls, dados_json: Dict[str, Any]) -> 'TEstrutura':
        """
        [CLASSE] Converte um dicionário de dados (JSON) em um novo objeto TEstrutura.
        Usa 'cls' para instanciar a classe.
        """
        # Note o uso de 'cls()' em vez de 'TEstrutura()'
        estrutura = cls()

        estrutura.id = dados_json.get("id", "")
        estrutura.QuantBase = dados_json.get("quantBase", 1) or 1
        estrutura.Nome = dados_json.get("nome", "")
        estrutura.AtivoBase = dados_json.get("ativoBase", "")
        estrutura.ValorAtivoIn = dados_json.get("valorAtivoIn", 0.0) or dados_json.get("valor_ativo_in") or 0.0
        estrutura.Sigma = dados_json.get("sigma", 0.0)
        estrutura.ValorIn = dados_json.get("valorIn", 0.0)
        estrutura.ValorAim = dados_json.get("valorAim", 0.0) or dados_json.get("valor_aim") or 0.0
        estrutura.ValorOut = dados_json.get("valorOut", 0.0)
        estrutura.DTE = dados_json.get("DTE") or dados_json.get("dte") or 0
        estrutura.MenorVencimento = dados_json.get("MenorVencimento") or 0
        estrutura.MaiorVencimento = dados_json.get("MaiorVencimento") or 0
        estrutura.Info = dados_json.get("Info") or dados_json.get("info") or ""
        try:
            estrutura.Categoria = TCategoria(dados_json.get("categoria", ""))
        except ValueError:
            estrutura.Categoria = TCategoria.SEM_CATEGORIA
        estrutura.Corretora = dados_json.get("corretora", "")

        for op_data in dados_json.get("opcoes", []):
            op = TOption()
            op.Quant = op_data.get("quant", 0)
            op.Tipo = op_data.get("tipo", "")
            op.Oper = op_data.get("oper", "C")
            op.Nome = op_data.get("nome", "")
            op.strike = op_data.get("strike", 0.0)
            op.Vencimento = op_data.get("vencimento_dias", 0)
            op.ValorIn = op_data.get("valorIn", 0.0)
            op.ValorBid = op_data.get("valorBid", 0.0)
            op.ValorAsk = op_data.get("valorAsk", 0.0)
            op.ValorLast = op_data.get("valorLast", 0.0)
            try:
                op.Status = TOptionStatus(op_data.get("status", "Ativa"))
            except ValueError:
                op.Status = TOptionStatus.ATIVA
            #op.expirada = op_data.get("expirada", False)
            estrutura.Options.Add(op)

        return estrutura

    # ---------------------------------------------------------------------

    @classmethod # O @classmethod é tecnicamente desnecessário aqui, mas mantém a simetria com o método acima
    def ConverterEstruturaParaEstruturaJSON(cls, estrutura: 'TEstrutura') -> Dict[str, Any]:
        """
        [CLASSE] Converte um objeto TEstrutura em um dicionário de dados (JSON) serializável.
        O parâmetro é a instância da estrutura a ser serializada.
        """
        return {
            "id": estrutura.id,
            "quantBase": estrutura.QuantBase,
            "nome": estrutura.Nome,
            "ativoBase": estrutura.AtivoBase,
            "valorAtivoIn": estrutura.ValorAtivoIn,
            "sigma": estrutura.Sigma,
            "valorIn": estrutura.ValorIn,
            "valorAim": estrutura.ValorAim,
            "valorOut": estrutura.ValorOut,
            "DTE": estrutura.DTE,             # Salva como Maiúsculo
            "info": estrutura.Info,           # Salva como Maiúsculo
            "MenorVencimento": estrutura.MenorVencimento,
            "MaiorVencimento": estrutura.MaiorVencimento,
            "categoria": estrutura.Categoria.value,
            "corretora": estrutura.Corretora,
            "opcoes": [
                {
                    "quant": op.Quant,
                    "tipo": op.Tipo,
                    "oper": op.Oper,
                    "nome": op.Nome,
                    "strike": op.strike,
                    "vencimento_dias": op.Vencimento,
                    "valorIn": op.ValorIn,
                    "valorBid": op.ValorBid,
                    "valorAsk": op.ValorAsk,
                    "valorLast": op.ValorLast,
                    "status": op.Status.value,
                    #"expirada": op.expirada,
                }
                for op in estrutura.Options
            ]
        }

    # Est: Plotar
    def PlotarPayoff_Plotly(self, minv: float, maxv: float, CDI: float) -> str:
        import numpy as np
        import plotly.graph_objects as go
        from plotly.subplots import make_subplots

        # --- 1. AJUSTE DE VOLATILIDADE ---
        for op in self.Options:
            if op.Tipo != 'ativo':
                if op.Volatilidade <= 0:
                    op.Volatilidade = self.Sigma if self.Sigma > 0.25 else 0.25

        # --- 2. PREPARAÇÃO DE PONTOS ---
        precos_base = np.linspace(minv, maxv, 200)
        strikes = sorted(list(set([op.strike for op in self.Options if op.strike > 0])))
        precos_finais = sorted(list(set(list(precos_base) + strikes)))

        # --- 3. CÁLCULO DAS CURVAS ---
        payoffs_venc = [self.CalcPayoffnoVencimento(preco, CDI) for preco in precos_finais]
        payoffs_now = [self.BlackScholesSimple(preco, 0, CDI) for preco in precos_finais]

        # --- SINCRONIZAÇÃO MATEMÁTICA DOS EIXOS (Alinhamento do Zero) ---
        y_tudo = payoffs_venc + payoffs_now
        ymin_r, ymax_r = min(y_tudo), max(y_tudo)
        # Adiciona margem de 15% para as linhas não encostarem nas bordas
        margem = (ymax_r - ymin_r) * 0.15 if ymax_r != ymin_r else 1.0
        ymin_r -= margem
        ymax_r += margem

        divisor = abs(self.ValorIn/self.QuantBase) if abs(self.ValorIn) > 0.001 else 1.0
        fator = 100 / divisor

        # Cálculo das porcentagens para o hover
        perc_venc = [(v * fator) for v in payoffs_venc]
        perc_now = [(v * fator) for v in payoffs_now]
        y_atual = (self.ValorNow - self.ValorIn)/self.QuantBase
        perc_atual = (y_atual * fator)

        # ==================== 4. CONFIGURAÇÃO DA FIGURA ===============================
        fig = make_subplots(specs=[[{"secondary_y": True}]])


        # --- 5. TRACE FANTASMA PARA ATIVAR O 2º EIXO ---
        fig.add_trace(go.Scatter(
            x=[precos_finais[0]], y=[ymin_r * fator],
            mode='markers', visible=False, showlegend=False, hoverinfo='skip'
        ), secondary_y=True)


        # ============== linhas horizontais e verticais ===================

        # LINHA ZERO
        fig.add_trace(go.Scatter(
            x=[minv, maxv],
            y=[0, 0],
            mode='lines',
            line=dict(color='black', width=1.5),
            showlegend=False,
            hoverinfo='skip'
        ), secondary_y=False)


        # --- Linha alvo
        if hasattr(self, 'ValorAim') and self.ValorAim != 0:
            y_alvo = (self.ValorAim - self.ValorIn) / self.QuantBase

            # Adiciona a linha como um Scatter (Trace)
            fig.add_trace(go.Scatter(
                x=[minv, maxv], # Define o início e fim da linha no eixo X
                y=[y_alvo, y_alvo],
                mode='lines',
                line=dict(color='purple', width=2.0, dash='5,5'), # 'dash' é equivalente ao 5,4
                name='Alvo',
                showlegend=False,
                hoverinfo='skip' # Para não atrapalhar o hover das curvas principais
            ), secondary_y=False)

            # Mantemos a anotação para o texto aparecer
            fig.add_annotation(
                xref="paper",
                x=0.01,
                y=y_alvo,
                yshift=10,
                text=f"Alvo: {y_alvo:.2f}",
                showarrow=False,
                font=dict(color="purple", size=12),
                bgcolor="white"
            )


        # --- Ativo Entrada (Linha e depois Marcador)
        if self.ValorAtivoIn > 0:
            # 1. Linha Vertical (Trace) - Adicionada primeiro para ficar atrás
            fig.add_trace(go.Scatter(
                x=[self.ValorAtivoIn, self.ValorAtivoIn],
                y=[ymin_r, ymax_r],
                mode='lines',
                line=dict(color='green', width=2, dash='dot'),
                showlegend=False,
                hoverinfo='skip'
            ), secondary_y=False)

            # 2. Marcador (Trace) - Adicionado depois para ficar na frente da linha
            fig.add_trace(go.Scatter(
                x=[self.ValorAtivoIn],
                y=[0],
                mode='markers',
                marker=dict(
                    symbol='circle-x',
                    size=10,
                    color='yellow',
                    line=dict(width=1.5, color='black')
                ),
                name='Ativo (Ent.)',
                hoverinfo='skip'
            ), secondary_y=False)


        # --- Ativo Atual (Linha e depois Marcador)
        if self.ValorAtivoNow > 0:
            # 1. Linha Vertical (Trace)
            fig.add_trace(go.Scatter(
                x=[self.ValorAtivoNow, self.ValorAtivoNow],
                y=[ymin_r, ymax_r],
                mode='lines',
                line=dict(color='green', width=2, dash='dot'),
                showlegend=False,
                hoverinfo='skip'
            ), secondary_y=False)

            # 2. Marcador (Trace)
            fig.add_trace(go.Scatter(
                x=[self.ValorAtivoNow],
                y=[0],
                mode='markers',
                marker=dict(
                    symbol='circle-dot',
                    size=10,
                    color='yellow',
                    line=dict(width=1.5, color='black')
                ),
                name='Ativo (Atual)',
                hoverinfo='skip'
            ), secondary_y=False)


        # --- Sigmas
        if self.Sigma > 0 and self.ValorAtivoIn > 0:
            maior_v, menor_v = self.GetVencimentos()
            dias_proj = menor_v if menor_v > 0 else 21
            vol_reais = self.ValorAtivoIn * self.Sigma * np.sqrt(dias_proj)

            for i in range(-3, 4):
                if i == 0: continue
                sigma_val = self.ValorAtivoIn + i * vol_reais

                # Substitui add_vline por add_trace
                fig.add_trace(go.Scatter(
                    x=[sigma_val, sigma_val],
                    y=[ymin_r, ymax_r],
                    mode='lines',
                    line=dict(color='orangered', dash='5,3', width=1), # dash='dash' ou '5px,3px'
                    showlegend=False,
                    hoverinfo='skip'
                ), secondary_y=False)

                # Mantém a anotação (texto fixo no rodapé do gráfico)
                fig.add_annotation(
                    x=sigma_val,
                    y=0.02,
                    yref="paper",
                    text=f"{i}σ",
                    showarrow=False,
                    font=dict(color="orangered", size=11),
                    bgcolor="white"
                )


        # ============= Curvas de payoff =======================

        # Adiciona Curva Vencimento (Eixo R$)
        fig.add_trace(go.Scatter(
            x=precos_finais, y=payoffs_venc, name='Vencimento', customdata=perc_venc,
            line=dict(color='blue', width=2.5),
            hovertemplate="Venc: R$%{y:.2f} (%{customdata:.2f}%)<extra></extra>"
        ), secondary_y=False)

        # Adiciona Curva Agora (Eixo R$)
        fig.add_trace(go.Scatter(
            x=precos_finais, y=payoffs_now, name='Atual', customdata=perc_now,
            line=dict(color='blue', width=2.5, dash='5,4'),
            hovertemplate="Atual: R$%{y:.2f} (%{customdata:.2f}%)<extra></extra>"
        ), secondary_y=False)



        # ============= Opções =======================
        for op in self.Options:
            if op.strike > 0:
                symbol = 'triangle-right' if op.Tipo == 'call' else 'triangle-left'
                fig.add_trace(go.Scatter(
                    x=[op.strike], y=[0], mode='markers',
                    marker=dict(symbol=symbol, color='orangered', size=10, line=dict(width=0.5, color='black')),
                    showlegend=False, hoverinfo='skip'
                ), secondary_y=False)
                fig.add_vline(x=op.strike, line=dict(color='orangered', dash='dot', width=1), opacity=0.7)



        # ============= Lucro atual =======================
        x_atual = self.ValorAtivoNow if self.ValorAtivoNow > 0 else self.ValorAtivoIn
        fig.add_trace(go.Scatter(x=[x_atual], y=[y_atual], mode='markers', customdata=[perc_atual],
            marker=dict(symbol='square', size=10, color='blue'), name='P&L Atual',
            hovertemplate="P&L: R$%{y:.2f} (%{customdata:.2f}%)<extra></extra>"), secondary_y=False)
        print('\n\nx_atual',x_atual,'y_atual',y_atual)


        # --- 7. CONFIGURAÇÃO FINAL DE EIXOS ---
        fig.update_xaxes(title="Preço do Ativo",
            range=[minv, maxv],
            showline=True, mirror=True, linecolor='black', gridcolor='#eee',
            layer="below traces",
            hoverformat=".2f")

        # Eixo Esquerdo
        fig.update_yaxes(
            title="P&L Financeiro (R$)",
            range=[ymin_r, ymax_r],
            showline=True,
            mirror=True,
            linecolor='black',
            gridcolor='#eee',

            # --- ADICIONE/AJUSTE ESTAS LINHAS ---
            zeroline=True,          # Ativa a linha no zero
            zerolinecolor='black',  # Cor da linha zero
            zerolinewidth=1.5,      # Grossura da linha zero
            layer="below traces",   # Garante que grid e zeroline fiquem atrás
            # ------------------------------------

            secondary_y=False
        )

        # Eixo Direito (Percentual) - Sincronizado pelo fator
        fig.update_yaxes(
            title="P&L Percentual (%)",
            side="right",
            range=[ymin_r * fator, ymax_r * fator],
            showline=True, linecolor='black', showgrid=False,
            secondary_y=True
        )

        fig.update_layout(
            hovermode="x unified",
            template="plotly_white",
            legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
            margin=dict(l=50, r=60, t=80, b=50),
            title=f"Payoff: {self.Nome} (@ {self.GetMenorVencimento()} dias)"
        )

        return fig.to_html(full_html=False, include_plotlyjs=False)



##    def PlotarPayoff_Plotly(self, minv: float, maxv: float, CDI: float) -> str:
##        print('\n\nPlotarPayoff_Plotly: ')
##        print('\n\nPlotarPayoff_Plotly: ', self.txt())
##        import numpy as np
##        import plotly.graph_objects as go
##        from plotly.subplots import make_subplots
##
##        # --- 1. AJUSTE DE VOLATILIDADE (CÓPIA EXATA DO SEU SVG) ---
##        for op in self.Options:
##            if op.Tipo != 'ativo':
##                if op.Volatilidade <= 0:
##                    op.Volatilidade = self.Sigma if self.Sigma > 0.25 else 0.25
##
##        # --- 2. PREPARAÇÃO DE PONTOS (CÓPIA EXATA DO SEU SVG) ---
##        precos_base = np.linspace(minv, maxv, 200)
##        strikes = sorted(list(set([op.strike for op in self.Options if op.strike > 0])))
##        precos_finais = sorted(list(set(list(precos_base) + strikes)))
##
##        # --- 3. CÁLCULO DAS CURVAS (CÓPIA EXATA DO SEU SVG) ---
##        # "THEN" (Vencimento)
##        payoffs_venc = [self.CalcPayoffnoVencimento(preco, CDI) for preco in precos_finais]
##        # "NOW" (Agora T+0) - Aqui o tempo passado é 0
##        payoffs_now = [self.BlackScholesSimple(preco, 0, CDI) for preco in precos_finais]
##
##        # --- 4. CONFIGURAÇÃO DA FIGURA ---
##        fig = make_subplots(specs=[[{"secondary_y": True}]])
##
##
##        # Adiciona Curva Vencimento (Azul Sólida - linestyle '-')
##        fig.add_trace(go.Scatter(
##            x=precos_finais, y=payoffs_venc, name='Vencimento',
##            line=dict(color='blue', width=2.5),
##            #hovertemplate="Ativo: %{x:.2f}<br>P&L: R$%{y:.2f}"
##            hovertemplate="Venc: R$%{y:.2f}<extra></extra>"
##            #hovertemplate="Ativo: %{x:.2f}<br>Venc: R$%{y:.2f}<extra></extra>"
##        ), secondary_y=False)
##
##        # Adiciona Curva Agora (Azul Tracejada - linestyle '--')
##        fig.add_trace(go.Scatter(
##            x=precos_finais, y=payoffs_now, name='Agora (T+0)',
##            line=dict(color='blue', width=2.5, dash='5,4'),
##            #hovertemplate="Ativo: %{x:.2f}<br>P&L: R$%{y:.2f}"
##            hovertemplate="Atual: R$%{y:.2f}<extra></extra>"
##        ), secondary_y=False)
##
##
##        # --- 6. ELEMENTOS VISUAIS (Plotar_Strikes_ValorAtivo_Sigma_Matplotlib_SVG) ---
##
##        # Strikes (triângulos e linhas pontilhadas)
##        for op in self.Options:
##            if op.strike > 0:
##                symbol = 'triangle-right' if op.Tipo == 'call' else 'triangle-left'
##                fig.add_trace(go.Scatter(
##                    x=[op.strike], y=[0], mode='markers',
##                    marker=dict(symbol=symbol, color='orangered', size=10, line=dict(width=0.5, color='black')),
##                    showlegend=False, hoverinfo='skip'
##                ), secondary_y=False)
##                fig.add_vline(x=op.strike, line=dict(color='orangered', dash='dot', width=1), opacity=1.0)
##
##        # Sigmas (Linhas laranjas e anotações)
##        if self.Sigma > 0 and self.ValorAtivoIn > 0:
##            maior_v, menor_v = self.GetVencimentos()
##            dias_proj = menor_v if menor_v > 0 else 21
##            vol_reais = self.AtivoBase_Sigma_Reais = self.ValorAtivoIn * self.Sigma * np.sqrt(dias_proj)
##            for i in range(-3, 4):
##                if i == 0: continue
##                sigma_val = self.ValorAtivoIn + i * vol_reais
##                fig.add_vline(x=sigma_val, line=dict(color='orangered', dash='5,3', width=1), opacity=1.0)
##                fig.add_annotation(x=sigma_val, y=0.02, yref="paper", text=f"{i}σ", showarrow=False, font=dict(color="orangered", size=12), bgcolor="white")
##
##        # Alvo (Linha Roxa)
##        if hasattr(self, 'ValorAim') and self.ValorAim != 0:
##            y_alvo = self.ValorAim - self.ValorIn
##            fig.add_hline(y=y_alvo, line=dict(color='purple', dash='5,4', width=2.0), opacity=1.0)
##            fig.add_annotation(xref="paper", x=0.01, y=y_alvo, yshift=10, text=f"Alvo: {y_alvo:.2f}", showarrow=False, font=dict(color="purple", size=12), bgcolor="white")
##
##        # Ponto de Entrada (Círculo preto) e Linha Verde (Spot Entrada)
##        if self.ValorAtivoIn > 0:
##            fig.add_trace(go.Scatter(
##                x=[self.ValorAtivoIn],
##                y=[0],
##                mode='markers',
##                marker=dict(symbol='circle-open', size=12, color='black', line=dict(width=2)),
##                name='Entrada',
##                # Se quiser esconder do hover para manter a caixa compacta, use hoverinfo='skip'
##                # Se quiser mostrar, o local correto do hovertemplate é aqui (fora do marker):
##                hovertemplate="Entrada: R$%{x:.2f}<extra></extra>"
##            ), secondary_y=False)
##
##            fig.add_vline(x=self.ValorAtivoIn, line=dict(color='green', dash='dot', width=2), opacity=1.0)
##
##        # P&L Atual (Quadrado azul)
##        x_atual = self.ValorAtivoNow if self.ValorAtivoNow > 0 else self.ValorAtivoIn
##        y_atual = (self.ValorNow/self.QuantBase - self.ValorIn)
##
##        fig.add_trace(go.Scatter(
##            x=[x_atual],
##            y=[y_atual],
##            mode='markers',
##            marker=dict(symbol='square', size=10, color='blue'), # Marker fechado aqui
##            name='P&L Atual',
##            # CORREÇÃO: hovertemplate movido para fora do marker
##            hovertemplate="P&L: R$%{y:.2f}<extra></extra>"
##        ), secondary_y=False)
##
##        # --- 6. SINCRONIZAÇÃO DE EIXOS (ESSENCIAL PARA NÃO SOBREPOR) ---
##        qb = abs(self.QuantBase) if self.QuantBase != 0 else 1
##        # O divisor para o eixo % é o custo inicial por ação (ValorIn)
##        divisor_perc = self.ValorIn if abs(self.ValorIn) > 0.01 else 1.0
##        fator = 100 / divisor_perc
##
##        # Pegamos os limites reais do P&L para casar as escalas
##        y_all = payoffs_venc + payoffs_now
##        y_min, y_max = min(y_all), max(y_all)
##
##        # --- 6. CONFIGURAÇÃO FINAL DE EIXOS E LAYOUT ---
##
##        # Unificando as configurações do Eixo X
##        fig.update_xaxes(
##            title="Preço do Ativo",
##            range=[minv, maxv],
##            showline=True,
##            mirror=True,
##            linecolor='black',
##            linewidth=1.5,
##            #
##            gridcolor='lightgray',     # Cor do grid (ajustada para aparecer melhor)
##            gridwidth=1.0,
##            griddash='dot',        # 'dash', 'dot', ou '5,3'
##            zeroline=True,
##            zerolinecolor='gray',
##            zerolinewidth=1.3
##        )
##
##        # Unificando as configurações do Eixo Y (Principal)
##        fig.update_yaxes(
##            title="P&L Financeiro (R$)",
##            showline=True,
##            mirror=True,
##            linecolor='black',
##            linewidth=1.5,
##            #
##            gridcolor='lightgray',
##            gridwidth=1.0,
##            griddash='dot',        # 'dash', 'dot', ou '5,3'
##            zeroline=True,
##            zerolinecolor='gray',
##            zerolinewidth=1.3,
##            secondary_y=False
##        )
##
##        # Configurações do Layout Global
##        fig.update_layout(
##            hovermode="x unified",  # Agrupa os valores e cria o cabeçalho
##            xaxis=dict(
##                title="Preço do Ativo",
##                # ESTA LINHA ABAIXO DEFINE O TOPO DO RETÂNGULO:
##                hoverformat=".2f",     # Mostra apenas 2 casas decimais (ex: 56.77)
##                gridcolor="#eee"
##            ),
##            # REMOVE OS ÍCONES GIGANTES DE INTERSEÇÃO (Círculos que seguem o mouse)
##            #hoverdistance=1,
##            #spikedistance=1,
##            #
##            title=f"Payoff: {self.Nome} (@ {self.GetMenorVencimento()} dias)",
##            template="plotly_white", # Aplicar o template antes das customizações
##            plot_bgcolor='white',
##            paper_bgcolor='white',
##            showlegend=True,
##            legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
##            margin=dict(l=50, r=50, t=80, b=50),
##
##            # Configuração do Eixo Y Secundário (%)
##            yaxis2=dict(
##                title="P&L Percentual (%)",
##                overlaying='y',
##                side='right',
##                range=[y_min * fator, y_max * fator],
##                showgrid=False # Geralmente deixamos o grid do eixo secundário oculto para não poluir
##            )
##        )
##
##        # Adiciona a linha do horizonte (Zero) explicitamente se necessário
##        #fig.add_hline(y=0, line_color="black", line_width=1.5)
##
##        return fig.to_html(full_html=False, include_plotlyjs=False)

# --- End of TEstrutura class ---
#----------------------------------------------------------------------------------------------------------------
    def SaveValorIn(self):
        """Salva o ValorIn (preço da transação) no TransacaoValue."""
        for option in self.Options:
            option.TransacaoValue = option.ValorIn

    def RestoreValorIn(self):
        """
        Restaura o ValorIn com base no TransacaoValue e ajusta Bid/Ask
        para refletir o preço de transação.
        """
        for option in self.Options:
            # 1. Restaura o ValorIn com o preço da transação
            option.ValorIn = option.TransacaoValue

            # 2. Se for uma COMPRA (Quant > 0):
            #    O ValorIn (Transação) é o preço que pagamos, o ValorAsk deve ser esse preço.
            if option.Quant > 0:
                option.ValorAsk = option.ValorIn
            # 3. Se for uma VENDA (Quant < 0):
            #    O ValorIn (Transação) é o preço que recebemos, o ValorBid deve ser esse preço.
            elif option.Quant < 0:
                option.ValorBid = option.ValorIn

            # Nota: O ValorIn será usado por CalcValorIn() para calcular o custo/receita total.


# --- Global helper functions (from the original Delphi unit) ---
def calcSigma(ValorAtivoIn: float, Sigma: float, dias: int, indice: int) -> float:
    # Ensure `dias` is positive for sqrt
    if dias <= 0:
        dias = 1e-10
    volatility_for_n_days = Sigma * math.sqrt(dias)
    volatilidade = volatility_for_n_days * ValorAtivoIn
    return ValorAtivoIn + indice * volatilidade