Overall Statistics
Total Orders
860
Average Win
1.33%
Average Loss
-0.75%
Compounding Annual Return
4.981%
Drawdown
22.300%
Expectancy
0.451
Start Equity
1000000
End Equity
1989381.23
Net Profit
98.938%
Sharpe Ratio
0.211
Sortino Ratio
0.259
Probabilistic Sharpe Ratio
1.160%
Loss Rate
48%
Win Rate
52%
Profit-Loss Ratio
1.77
Alpha
-0.006
Beta
0.236
Annual Standard Deviation
0.072
Annual Variance
0.005
Information Ratio
-0.598
Tracking Error
0.123
Treynor Ratio
0.064
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
RUT Z1OK4G4RF6FI|RUT 31
Portfolio Turnover
0.07%
Drawdown Recovery
1292
from AlgorithmImports import *
from datetime import timedelta
import numpy as np
from scipy.stats import norm

class EuropeanOptionsArbitrageAlgorithm(QCAlgorithm):
    
    def initialize(self):
        self.set_start_date(2012, 1, 1)
        self.set_cash(1000000)
        
        # Evropské indexy
        self._symbols = ["SPX", "NDX", "RUT"]
        self._underlying = {}
        self._option_chains = {}
        
        # Přidání indexů a jejich opcí
        for symbol_str in self._symbols:
            index = self.add_index(symbol_str, Resolution.MINUTE)
            self._underlying[symbol_str] = index.symbol
            
            # Přidání opcí - evropské opce
            option = self.add_index_option(index.symbol, Resolution.MINUTE)
            option.set_filter(lambda u: u.include_weeklys().strikes(-1, 1).expiration(0, 60))
            self._option_chains[symbol_str] = option.symbol
        
        # Black-Scholes kalkulátor
        self._bs_calculator = BlackScholesCalculator()
        
        # Sledování otevřených pozic
        self._open_positions = {}
        
        # Naplánování kontroly před zavřením trhu (15:45 EST)
        self.schedule.on(
            self.date_rules.every_day(),
            self.time_rules.at(15, 45),
            self._check_arbitrage_opportunities
        )
        
        # Nastavení pro seed prices
        self.settings.seed_initial_prices = True
        
        self.debug("Algoritmus inicializován pro arbitráž evropských opcí")
    
    def _check_arbitrage_opportunities(self):
        """Kontrola arbitrážních příležitostí před zavřením trhu"""
        
        # Kontrola cash reserve (minimálně 80%)
        cash_ratio = self.portfolio.cash / self.portfolio.total_portfolio_value
        if cash_ratio < 0.80:
            self.debug(f"Cash reserve ({cash_ratio:.1%}) pod 80%, žádné nové pozice")
            return
        
        for symbol_str in self._symbols:
            # Kontrola zda už máme otevřenou pozici na tomto tickeru
            if self._has_open_position_for_ticker(symbol_str):
                continue
            
            underlying_symbol = self._underlying[symbol_str]
            
            # Získání aktuální ceny underlying
            if not self.securities.contains_key(underlying_symbol):
                continue
                
            underlying_price = self.securities[underlying_symbol].price
            if underlying_price == 0:
                continue
            
            # Získání option chain
            option_symbol = self._option_chains[symbol_str]
            chain = self.current_slice.option_chains.get(option_symbol)
            
            if chain is None or len(chain) == 0:
                continue
            
            # Odhad implicitní volatility z ATM opcí
            iv = self._estimate_implied_volatility(chain, underlying_price)
            if iv == 0:
                continue
            
            # Risk-free rate (aktuální)
            risk_free_rate = 0.05  # 5% aproximace
            
            # Kontrola každé opce v chainu
            for contract in chain:
                # Kontrola likvidity
                if contract.bid_price == 0 or contract.ask_price == 0:
                    continue
                
                # Výpočet času do expirace
                time_to_expiry = (contract.expiry - self.time).days / 365.0
                if time_to_expiry <= 0:
                    continue
                
                # Tržní cena (bid pro nákup, použijeme ask protože kupujeme)
                market_price = contract.ask_price
                
                # Výpočet Black-Scholes fair value
                if contract.right == OptionRight.CALL:
                    bs_price = self._bs_calculator.call_option(
                        underlying_price,
                        contract.strike,
                        time_to_expiry,
                        risk_free_rate,
                        iv
                    )
                else:  # PUT
                    bs_price = self._bs_calculator.put_option(
                        underlying_price,
                        contract.strike,
                        time_to_expiry,
                        risk_free_rate,
                        iv
                    )
                
                # Kontrola arbitrážní příležitosti
                # Kupujeme když je tržní cena nižší než BS fair value
                discount = (bs_price - market_price) / bs_price if bs_price > 0 else 0
                
                if discount > 0.10:  # Minimálně 10% discount pro arbitráž
                    self._execute_arbitrage(contract, market_price, bs_price, underlying_price, symbol_str)
    
    def _has_open_position_for_ticker(self, ticker):
        """Kontrola zda existuje otevřená pozice pro daný ticker"""
        for symbol, position in self._open_positions.items():
            if position.get('ticker') == ticker:
                return True
        return False
    
    def _estimate_implied_volatility(self, chain, underlying_price):
        """Odhad průměrné IV z ATM opcí"""
        atm_ivs = []
        
        for contract in chain:
            # Pouze ATM opce (±5% od underlying)
            if abs(contract.strike - underlying_price) / underlying_price < 0.05:
                if hasattr(contract, 'implied_volatility') and contract.implied_volatility > 0:
                    atm_ivs.append(contract.implied_volatility)
        
        if len(atm_ivs) > 0:
            return np.mean(atm_ivs)
        
        return 0.25  # Default 25% pokud není k dispozici
    
    def _execute_arbitrage(self, contract, market_price, bs_price, underlying_price, ticker):
        """Provedení arbitrážního obchodu"""
        
        # Kontrola zda už nemáme pozici na tomto tickeru
        if self._has_open_position_for_ticker(ticker):
            return
        
        # Kontrola zda už nemáme tuto konkrétní pozici
        if contract.symbol in self._open_positions:
            return
        
        # Maximálně 5% portfolia na jeden trade
        max_position_value = self.portfolio.total_portfolio_value * 0.01
        
        # Počet kontraktů (každý kontrakt = 100 akcií)
        contract_value = market_price * 100
        if contract_value == 0:
            return
            
        quantity = int(max_position_value / contract_value)
        
        if quantity < 1:
            return
        
        # Market order nákup
        ticket = self.market_order(contract.symbol, quantity)
        
        if ticket.status == OrderStatus.FILLED:
            self._open_positions[contract.symbol] = {
                'ticker': ticker,
                'quantity': quantity,
                'entry_price': market_price,
                'bs_price': bs_price,
                'expiry': contract.expiry,
                'underlying': underlying_price,
                'strike': contract.strike,
                'right': contract.right
            }
            
            self.debug(f"ARBITRÁŽ {ticker}: {contract.symbol} | Množství: {quantity} | "
                      f"Tržní cena: ${market_price:.2f} | BS cena: ${bs_price:.2f} | "
                      f"Discount: {((bs_price - market_price) / bs_price * 100):.1f}%")
    
    def on_data(self, data):
        """Event handler pro příchozí data"""
        # Kontrola expirovaných opcí
        expired_symbols = []
        for symbol, position in self._open_positions.items():
            if self.time >= position['expiry']:
                expired_symbols.append(symbol)
        
        # Odstranění expirovaných pozic ze sledování
        for symbol in expired_symbols:
            if symbol in self._open_positions:
                position = self._open_positions[symbol]
                ticker = position.get('ticker', 'N/A')
                self.debug(f"EXPIROVÁNO {ticker}: {symbol} | Drženo do splatnosti")
                del self._open_positions[symbol]


class BlackScholesCalculator:
    """Black-Scholes model pro oceňování evropských opcí"""
    
    def call_option(self, S, K, T, r, sigma):
        """
        Výpočet ceny evropské call opce
        S: Aktuální cena underlying
        K: Strike price
        T: Čas do expirace (roky)
        r: Risk-free rate
        sigma: Volatilita (IV)
        """
        if T <= 0 or sigma <= 0:
            return max(S - K, 0)  # Intrinsic value
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        call_price = S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
        return max(call_price, 0)
    
    def put_option(self, S, K, T, r, sigma):
        """
        Výpočet ceny evropské put opce
        S: Aktuální cena underlying
        K: Strike price
        T: Čas do expirace (roky)
        r: Risk-free rate
        sigma: Volatilita (IV)
        """
        if T <= 0 or sigma <= 0:
            return max(K - S, 0)  # Intrinsic value
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        put_price = K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)
        return max(put_price, 0)
    
    def delta(self, S, K, T, r, sigma, option_type='call'):
        """Výpočet delta (citlivost na změnu ceny underlying)"""
        if T <= 0 or sigma <= 0:
            return 0
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        
        if option_type.lower() == 'call':
            return norm.cdf(d1)
        else:  # put
            return norm.cdf(d1) - 1
    
    def gamma(self, S, K, T, r, sigma):
        """Výpočet gamma (citlivost delta na změnu ceny)"""
        if T <= 0 or sigma <= 0:
            return 0
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        return norm.pdf(d1) / (S * sigma * np.sqrt(T))
    
    def vega(self, S, K, T, r, sigma):
        """Výpočet vega (citlivost na změnu volatility)"""
        if T <= 0 or sigma <= 0:
            return 0
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        return S * norm.pdf(d1) * np.sqrt(T) / 100
    
    def theta(self, S, K, T, r, sigma, option_type='call'):
        """Výpočet theta (time decay)"""
        if T <= 0 or sigma <= 0:
            return 0
        
        d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
        d2 = d1 - sigma * np.sqrt(T)
        
        if option_type.lower() == 'call':
            theta = (-S * norm.pdf(d1) * sigma / (2 * np.sqrt(T)) 
                    - r * K * np.exp(-r * T) * norm.cdf(d2)) / 365
        else:  # put
            theta = (-S * norm.pdf(d1) * sigma / (2 * np.sqrt(T)) 
                    + r * K * np.exp(-r * T) * norm.cdf(-d2)) / 365
        
        return theta