The market is approaching its peak; how can one achieve an effective and cheap hedge?

I don't know if it's true, and I know that timing the market doesn't work, but I still have a feeling that a correction might be approaching. I have tried various solutions for hedging, ranging from using alternative strategies on AllocateSmartly (which I have now developed my own scripts to adapt to some ideas from that site) to an attempt at implementing Yuval T's put strategy (I have attached the script I used).

But does anyone have success with good yet inexpensive hedging strategies? Does anyone use, for example, Short ETF solutions for effective hedging, or other approaches?


# -*- coding: utf-8 -*-
"""

"""

import datetime as dt
import pandas as pd
import yfinance as yf
import numpy as np
from scipy.stats import norm, percentileofscore
from concurrent.futures import ThreadPoolExecutor
import logging
import time
from pathlib import Path

# ============================================================================
# KONFIGURASJON - JUSTER DISSE ETTER BEHOV
# ============================================================================

# Taylor-spesifikke parametre
TAYLOR_BS_DISCOUNT = 0.60           # Taylor betaler typisk 40-60% av B-S pris (justert opp)
MAX_IV_RANK = 70                    # Kjøp kun nür IV rank < 70 (mer fleksibelt)
MIN_EXPECTED_ANNUAL_RETURN = 0.30   # Minimum 30% forventet ĂĽrlig avkastning (mer realistisk)

# Tidsramme (fra artikkel)
MIN_EXP_MONTHS = 3                  # Minimum 3 mĂĽneder (Taylor: >2-3 mnd)
MAX_EXP_MONTHS = 9                  # Maksimum 9 mĂĽneder (Taylor: <9 mnd)

# Likviditet (lavere krav - Taylor bruker GTC orders uansett)
MIN_OPEN_INTEREST = 1               # Minimum 1 (kan bruke GTC orders)
MIN_VOLUME = 0                      # Ingen volum-krav (GTC orders)

# Performance
MAX_WORKERS = 2
SLEEP_TIME = 1.5
RISK_FREE_RATE = 0.045              # Oppdatert risikofri rente (4.5%)

# Output - KUN terminal, ingen CSV
DISPLAY_TOP_N = 50  # Hvor mange opsjoner ĂĽ vise

# ============================================================================
# LOGGING
# ============================================================================

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

# ============================================================================
# HJELPEFUNKSJONER
# ============================================================================

def parse_scores(score_str):
    """Parser ticker + score string til dictionary"""
    scores = {}
    for line in score_str.strip().split('\n'):
        try:
            parts = line.split()
            if len(parts) >= 2:
                ticker = parts[0].strip()
                score = float(parts[-1])
                scores[ticker] = score
        except (ValueError, Exception) as e:
            logging.warning(f"Kunne ikke parse: '{line}' - {e}")
    return scores


def normalize_scores(scores):
    """Normaliser scores (0-1), høyere = dürligere aksje"""
    if not scores:
        return {}

    valid = {k: v for k, v in scores.items() if pd.notna(v)}
    if not valid:
        return {t: 0.5 for t in scores}

    max_s = max(valid.values())
    min_s = min(valid.values())

    if max_s == min_s:
        return {t: 0.5 for t in scores}

    return {
        t: (s - min_s) / (max_s - min_s) if pd.notna(s) else np.nan
        for t, s in scores.items()
    }


def filter_expirations(expirations, min_months=MIN_EXP_MONTHS, max_months=MAX_EXP_MONTHS):
    """Filtrer utløpsdatoer (3-9 müneder)"""
    now = dt.datetime.now()
    min_date = now + dt.timedelta(days=30 * min_months)
    max_date = now + dt.timedelta(days=30 * max_months)

    filtered = []
    for exp_str in expirations:
        try:
            exp_date = dt.datetime.strptime(exp_str, "%Y-%m-%d")
            if min_date <= exp_date <= max_date:
                filtered.append(exp_str)
        except ValueError:
            continue

    logging.debug(f"Filtrerte utløp: {len(filtered)} datoer mellom {min_months}-{max_months} mnd")
    return filtered


def calculate_historical_volatility(symbol, window='1y'):
    """Beregner annualisert historisk volatilitet"""
    try:
        tk = yf.Ticker(symbol)
        hist = tk.history(period=window)

        if hist.empty or len(hist) < 10:
            return np.nan

        hist['LogReturn'] = np.log(hist['Close'] / hist['Close'].shift(1))
        hist = hist.dropna(subset=['LogReturn'])

        if len(hist) < 10:
            return np.nan

        # Annualisert volatilitet (i desimaler, ikke prosent)
        vol = hist['LogReturn'].std() * np.sqrt(252)
        return vol

    except Exception as e:
        logging.error(f"Feil ved HV for {symbol}: {e}")
        return np.nan


def calculate_iv_rank(current_iv, historical_vol_1y, historical_vol_6m):
    """
    Beregner IV Rank - hvor billig er opsjonen?

    Taylor kjøper nür IV er LAV (billige opsjoner)
    IV Rank < 30 = Veldig billig
    IV Rank < 50 = Rimelig
    IV Rank > 60 = For dyrt
    """
    if pd.isna(current_iv) or pd.isna(historical_vol_1y):
        return np.nan

    # Sammenlign med historisk volatilitet
    # Hvis IV << HV, er opsjonene billige
    avg_hv = np.nanmean([historical_vol_1y, historical_vol_6m])

    if pd.isna(avg_hv) or avg_hv <= 0:
        return np.nan

    # IV Rank: hvor er IV relativt til HV? (0-100 skala)
    iv_rank = (current_iv / avg_hv) * 50  # Skalert til ~0-100

    return min(100, max(0, iv_rank))


def calculate_black_scholes_put(S, K, T, r, sigma):
    """Black-Scholes Put pris"""
    if any(pd.isna([S, K, T, r, sigma])) or sigma <= 1e-6 or T <= 1e-6:
        return np.nan

    try:
        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(0.0, put_price)

    except Exception as e:
        logging.debug(f"B-S feil: {e}")
        return max(0.0, K - S) if pd.notna(K) and pd.notna(S) else np.nan


def calculate_taylor_metrics(row, score, normalized_score, hv_1y, hv_6m):
    """
    Beregner Taylor-spesifikke metrikker for ĂŠn opsjon

    Returns: dict med alle nødvendige verdier
    """
    metrics = {}

    # Basis verdier
    S = row['stockPrice']
    K = row['strike']
    T = row['timeToMaturity']
    r = RISK_FREE_RATE
    IV = row['impliedVolatility'] / 100 if pd.notna(row['impliedVolatility']) else np.nan
    last_price = row['lastPrice']

    # 1. Black-Scholes pris med historisk volatilitet
    if pd.notna(hv_6m) and hv_6m > 0:
        bs_hv_price = calculate_black_scholes_put(S, K, T, r, hv_6m)
    else:
        bs_hv_price = np.nan

    metrics['bs_hv_price'] = bs_hv_price

    # 2. Taylor Max Price: Typisk 40-50% av B-S, justert for score
    #    Høyere score (dürligere aksje) = kan betale litt mer
    if pd.notna(bs_hv_price) and bs_hv_price > 0:
        # Base discount
        discount = TAYLOR_BS_DISCOUNT

        # Juster for score (normalisert 0-1)
        # Høyere normalized_score = dürligere aksje = litt høyere max pris
        score_adjustment = 1 + (normalized_score * 0.3)  # 1.0 til 1.3

        taylor_max = bs_hv_price * discount * score_adjustment
        metrics['taylor_max_price'] = taylor_max

        # Hvor billig er opsjonen vs. Taylors maks?
        if last_price > 0:
            metrics['price_vs_taylor_max'] = last_price / taylor_max
        else:
            metrics['price_vs_taylor_max'] = np.nan
    else:
        metrics['taylor_max_price'] = np.nan
        metrics['price_vs_taylor_max'] = np.nan

    # 3. IV Rank
    metrics['iv_rank'] = calculate_iv_rank(IV, hv_1y, hv_6m)

    # 4. Expected Return (forenklet)
    #    Antar at aksjen kan falle med 20-40% (basert pĂĽ score)
    #    Høyere score = større forventet fall
    if pd.notna(last_price) and last_price > 0 and pd.notna(S) and pd.notna(K):
        # Forventet prisfall basert pĂĽ score
        expected_drop_pct = 0.15 + (normalized_score * 0.25)  # 15% til 40%
        expected_price = S * (1 - expected_drop_pct)

        # Forventet payoff
        expected_payoff = max(0, K - expected_price)
        expected_profit = expected_payoff - last_price

        if expected_profit > 0:
            expected_return = expected_profit / last_price
            # Annualisert
            if T > 0:
                expected_annual_return = (1 + expected_return) ** (1/T) - 1
            else:
                expected_annual_return = np.nan
        else:
            expected_return = -1
            expected_annual_return = -1

        metrics['expected_return'] = expected_return
        metrics['expected_annual_return'] = expected_annual_return
    else:
        metrics['expected_return'] = np.nan
        metrics['expected_annual_return'] = np.nan

    # 5. Break-even pris
    if pd.notna(K) and pd.notna(last_price):
        metrics['breakeven_price'] = K - last_price
        if pd.notna(S) and S > 0:
            metrics['breakeven_drop_pct'] = (S - metrics['breakeven_price']) / S
        else:
            metrics['breakeven_drop_pct'] = np.nan
    else:
        metrics['breakeven_price'] = np.nan
        metrics['breakeven_drop_pct'] = np.nan

    return metrics


def process_ticker(symbol, score, normalized_score, risk_free_rate, sleep_time):
    """
    Hovedfunksjon: Analyser ĂŠn ticker og finn beste put-opsjoner

    Følger Taylors prinsipper:
    1. OTM puts, 3-9 mĂĽneder
    2. Kun nĂĽr IV er lav (billige opsjoner)
    3. MĂĽ vĂŚre billigere enn Taylor max price
    4. Forventet avkastning mü vÌre høy nok
    """
    logging.debug(f"Venter {sleep_time:.1f}s før {symbol}...")
    time.sleep(sleep_time)

    try:
        logging.info(f"=== Analyserer {symbol} (Score: {score:.2f}) ===")
        tk = yf.Ticker(symbol)

        # 1. Hent aksjekurs
        try:
            hist = tk.history(period="1d")
            if hist.empty or 'Close' not in hist.columns:
                logging.warning(f"Ingen kurs for {symbol}")
                return pd.DataFrame()

            current_price = hist['Close'].iloc[-1]
            if pd.isna(current_price):
                logging.warning(f"Kurs er NaN for {symbol}")
                return pd.DataFrame()

            logging.info(f"Kurs: ${current_price:.2f}")

        except Exception as e:
            logging.error(f"Feil ved henting av kurs for {symbol}: {e}")
            return pd.DataFrame()

        # 2. Beregn historisk volatilitet (1 ĂĽr og 6 mnd)
        hv_1y = calculate_historical_volatility(symbol, window='1y')
        hv_6m = calculate_historical_volatility(symbol, window='6mo')

        if pd.isna(hv_6m):
            logging.warning(f"Kunne ikke beregne HV for {symbol}")
            return pd.DataFrame()

        logging.info(f"HV 6m: {hv_6m*100:.1f}%, HV 1y: {hv_1y*100:.1f}%")

        # 3. Hent opsjonsdata
        try:
            expirations = tk.options
            if not expirations:
                logging.warning(f"Ingen opsjoner for {symbol}")
                return pd.DataFrame()
        except Exception as e:
            logging.error(f"Feil ved henting av opsjoner for {symbol}: {e}")
            return pd.DataFrame()

        filtered_exp = filter_expirations(expirations)
        if not filtered_exp:
            logging.warning(f"Ingen utløpsdatoer {MIN_EXP_MONTHS}-{MAX_EXP_MONTHS} mnd for {symbol}")
            return pd.DataFrame()

        # 4. Samle puts fra alle utløpsdatoer
        all_puts = []
        for exp_date in filtered_exp:
            try:
                time.sleep(sleep_time / 4)
                options = tk.option_chain(exp_date)
                puts = options.puts

                if puts is not None and not puts.empty:
                    puts['expirationDate'] = pd.to_datetime(exp_date)
                    all_puts.append(puts)

            except Exception as e:
                logging.error(f"Feil ved henting av puts for {symbol} ({exp_date}): {e}")
                continue

        if not all_puts:
            logging.warning(f"Ingen puts funnet for {symbol}")
            return pd.DataFrame()

        data = pd.concat(all_puts, ignore_index=True)

        # 5. Filtrer OTM puts
        otm_puts = data[data['strike'] < current_price * 0.995].copy()

        if otm_puts.empty:
            logging.info(f"Ingen OTM puts for {symbol}")
            return pd.DataFrame()

        logging.info(f"Fant {len(otm_puts)} OTM puts")

        # 6. Legg til basisinfo
        otm_puts['underlyingSymbol'] = symbol
        otm_puts['stockPrice'] = current_price
        otm_puts['originalScore'] = score
        otm_puts['normalizedScore'] = normalized_score

        # 7. Beregn tid til utløp
        now_dt = pd.Timestamp.now(tz='UTC')
        if otm_puts['expirationDate'].dt.tz is None:
            otm_puts['expirationDate'] = otm_puts['expirationDate'].dt.tz_localize('UTC')

        exp_dt = otm_puts['expirationDate'] + pd.Timedelta(hours=16)
        otm_puts['timeToMaturity'] = (exp_dt - now_dt).dt.total_seconds() / (365.25 * 24 * 3600)
        otm_puts = otm_puts[otm_puts['timeToMaturity'] > 0.01]

        if otm_puts.empty:
            return pd.DataFrame()

        # 8. Sikre numeriske verdier
        otm_puts['strike'] = pd.to_numeric(otm_puts['strike'], errors='coerce')
        otm_puts['lastPrice'] = pd.to_numeric(otm_puts['lastPrice'], errors='coerce')
        otm_puts['bid'] = pd.to_numeric(otm_puts['bid'], errors='coerce')
        otm_puts['ask'] = pd.to_numeric(otm_puts['ask'], errors='coerce')
        otm_puts['volume'] = pd.to_numeric(otm_puts['volume'], errors='coerce').fillna(0)
        otm_puts['openInterest'] = pd.to_numeric(otm_puts['openInterest'], errors='coerce').fillna(0)
        otm_puts['impliedVolatility'] = pd.to_numeric(otm_puts['impliedVolatility'], errors='coerce') * 100

        # 9. Filtrer pĂĽ likviditet
        initial_count = len(otm_puts)
        otm_puts = otm_puts[
            (otm_puts['openInterest'] >= MIN_OPEN_INTEREST) |
            (otm_puts['volume'] >= MIN_VOLUME)
        ]
        logging.info(f"Etter likviditetsfilter: {len(otm_puts)}/{initial_count}")

        if otm_puts.empty:
            return pd.DataFrame()

        # 10. TAYLOR-ANALYSE: Beregn alle metrikker
        taylor_metrics_list = []
        for idx, row in otm_puts.iterrows():
            metrics = calculate_taylor_metrics(row, score, normalized_score, hv_1y, hv_6m)
            taylor_metrics_list.append(metrics)

        # Legg til som nye kolonner
        metrics_df = pd.DataFrame(taylor_metrics_list)
        otm_puts = pd.concat([otm_puts.reset_index(drop=True), metrics_df], axis=1)

        # 11. TAYLOR FILTRE

        # Filter 1: IV Rank (kun advarsel, ikke hard filter)
        valid_iv = otm_puts['iv_rank'].notna()
        if valid_iv.any():
            high_iv_count = (otm_puts['iv_rank'] > MAX_IV_RANK).sum()
            if high_iv_count > 0:
                logging.info(f"⚠️  {high_iv_count} opsjoner har høy IV rank (>{MAX_IV_RANK}) - vær forsiktig")
            # IKKE filtrer bort - la bruker vurdere selv

        # Filter 2: Pris mĂĽ vĂŚre under Taylor max
        before = len(otm_puts)
        otm_puts = otm_puts[
            pd.notna(otm_puts['price_vs_taylor_max']) &
            (otm_puts['price_vs_taylor_max'] < 1.0)
        ]
        logging.info(f"Taylor max pris filter: {len(otm_puts)}/{before}")

        if otm_puts.empty:
            logging.info(f"Ingen opsjoner passerte Taylor-filtrene for {symbol}")
            return pd.DataFrame()

        # Filter 3: Forventet avkastning (advarsel, ikke hard filter)
        before = len(otm_puts)
        low_return = (
            pd.notna(otm_puts['expected_annual_return']) &
            (otm_puts['expected_annual_return'] < MIN_EXPECTED_ANNUAL_RETURN)
        ).sum()

        if low_return > 0:
            logging.info(f"⚠️  {low_return} opsjoner har lav forventet avkastning (<{MIN_EXPECTED_ANNUAL_RETURN*100:.0f}%)")

        # Behold alle, men merk de med lav avkastning
        otm_puts['meets_return_threshold'] = (
            otm_puts['expected_annual_return'] >= MIN_EXPECTED_ANNUAL_RETURN
        )

        if otm_puts.empty:
            logging.info(f"Ingen opsjoner passerte noen filtre for {symbol}")
            return pd.DataFrame()

        # 12. Beregn "Taylor Score" for rangering
        #     Kombinerer flere faktorer

        # Gi bonus til de som møter threshold
        return_bonus = otm_puts['meets_return_threshold'].astype(int) * 15

        otm_puts['taylor_score'] = (
            # Lavere pris vs max = bedre
            (1 - otm_puts['price_vs_taylor_max']) * 40 +

            # Lavere IV rank = bedre (hvis tilgjengelig)
            (1 - otm_puts['iv_rank'].fillna(50) / 100) * 25 +

            # Høyere forventet avkastning = bedre
            (otm_puts['expected_annual_return'].fillna(0).clip(upper=2.0) / 2.0) * 20 +

            # Høyere normalized score (dürligere aksje) = bedre
            otm_puts['normalizedScore'] * 10 +

            # Bonus for ü møte return threshold
            return_bonus
        )

        # Legg til kvalitetsindikator
        otm_puts['quality'] = 'OK'
        otm_puts.loc[otm_puts['price_vs_taylor_max'] < 0.7, 'quality'] = 'GOOD'
        otm_puts.loc[
            (otm_puts['price_vs_taylor_max'] < 0.5) &
            (otm_puts['iv_rank'].fillna(100) < 40),
            'quality'
        ] = 'EXCELLENT'

        logging.info(f"✅ Fant {len(otm_puts)} kvalifiserte puts for {symbol}")
        return otm_puts

    except Exception as e:
        logging.exception(f"Uventet feil for {symbol}: {e}")
        return pd.DataFrame()


# ============================================================================
# HOVEDPROGRAM
# ============================================================================

def main(score_str):
    """
    Hovedfunksjon som kjører hele analysen
    """

    print("\n" + "="*80)
    print("TAYLOR PUT OPTIONS SCREENER")
    print("="*80)
    print(f"\n⚙️  KONFIGURASJON:")
    print(f"   • Tidsramme: {MIN_EXP_MONTHS}-{MAX_EXP_MONTHS} måneder")
    print(f"   • Max pris: {TAYLOR_BS_DISCOUNT*100:.0f}% av Black-Scholes")
    print(f"   • IV Rank advarselsgrense: {MAX_IV_RANK}")
    print(f"   • Min forventet avkastning (anbefalt): {MIN_EXPECTED_ANNUAL_RETURN*100:.0f}% årlig")
    print(f"   • Parallelle tråder: {MAX_WORKERS}")
    print(f"   • Viser: Topp {DISPLAY_TOP_N} opsjoner")
    print("\n" + "="*80 + "\n")

    # Parse scores
    scores = parse_scores(score_str)
    normalized_scores = normalize_scores(scores)
    tickers = list(scores.keys())

    logging.info(f"Antall tickere: {len(tickers)}")

    # Kjør analyse
    results = []
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        tasks = []
        for symbol in tickers:
            if symbol in scores and symbol in normalized_scores:
                if pd.notna(scores[symbol]) and pd.notna(normalized_scores[symbol]):
                    tasks.append(
                        executor.submit(
                            process_ticker,
                            symbol,
                            scores[symbol],
                            normalized_scores[symbol],
                            RISK_FREE_RATE,
                            SLEEP_TIME
                        )
                    )

        logging.info(f"Sendt {len(tasks)} oppgaver til analyse...\n")

        for i, future in enumerate(tasks, 1):
            try:
                result = future.result()
                if result is not None and not result.empty:
                    results.append(result)

                if i % 10 == 0:
                    logging.info(f"Ferdig med {i}/{len(tasks)} tickere")

            except Exception as e:
                logging.error(f"Feil ved henting av resultat: {e}")

    # Analyser resultater
    if not results:
        print("\n⚠️  INGEN OPSJONER FUNNET")
        print("Mulige ĂĽrsaker:")
        print("  • Markedet er for dyrt (høy IV)")
        print("  • Ingen opsjoner møter Taylors strenge kriterier")
        print("  • Prøv å øke TAYLOR_BS_DISCOUNT eller MAX_IV_RANK")
        return

    # Kombiner alle resultater
    all_puts = pd.concat(results, ignore_index=True)
    logging.info(f"\n✅ Totalt {len(all_puts)} opsjoner funnet på tvers av {len(results)} aksjer")

    # Sorter pĂĽ Taylor Score
    all_puts = all_puts.sort_values('taylor_score', ascending=False)

    # Velg topp 3 per ticker (diversifisering)
    top_per_ticker = all_puts.groupby('underlyingSymbol').head(3)

    # Velg topp 30 totalt
    top_opportunities = all_puts.head(30)

    # ========================================================================
    # VISNING
    # ========================================================================

    display_cols = [
        'underlyingSymbol',
        'taylor_score',
        'quality',
        'originalScore',
        'contractSymbol',
        'lastPrice',
        'taylor_max_price',
        'price_vs_taylor_max',
        'strike',
        'stockPrice',
        'expirationDate',
        'timeToMaturity',
        'iv_rank',
        'expected_annual_return',
        'breakeven_price',
        'breakeven_drop_pct',
        'bid',
        'ask',
        'volume',
        'openInterest'
    ]

    display_cols = [c for c in display_cols if c in top_opportunities.columns]
    display_df = top_opportunities[display_cols].copy()

    # Formater datoer
    if 'expirationDate' in display_df:
        display_df['expirationDate'] = display_df['expirationDate'].dt.strftime('%Y-%m-%d')

    # Vis resultater
    print("\n" + "="*80)
    print(f"🎯 TOPP {DISPLAY_TOP_N} PUT-OPSJONER (Taylor-metoden)")
    print("="*80)
    print("\n📊 KOLONNE-FORKLARING:")
    print("  • CONTRACT: Fullt opsjonsnavn (bruk dette til å bestille)")
    print("  • T_SCORE: Taylor score (høyere = bedre kombinasjon av faktorer)")
    print("  • QUAL: Kvalitet (EXCELLENT/GOOD/OK basert på pris og IV)")
    print("  • F_SCORE: Din fundamental score (99.97 = verst aksje)")
    print("  • prc/max: Pris vs Taylor max (<0.5=SVÆRT billig, <0.8=Bra, <1.0=OK)")
    print("  • IV_RNK: IV rank (<30=Svært billig, <50=Billig, >60=Dyr)")
    print("  • exp_ret_%: Forventet årlig avkastning hvis aksjen faller som antatt")
    print("  • be_drop_%: Hvor mye må aksjen falle for break-even?")
    print("\n💡 Fokuser på: QUAL=EXCELLENT/GOOD + T_SCORE>60 + prc/max<0.8")
    print("-"*80 + "\n")

    # Vis alle resultater pent formatert
    pd.set_option('display.max_rows', None)
    pd.set_option('display.max_columns', None)
    pd.set_option('display.width', 250)  # Økt fra 200 for CONTRACT-kolonne
    pd.set_option('display.max_colwidth', 35)  # Max lengde for CONTRACT
    pd.set_option('display.float_format', '{:.2f}'.format)

    # Formater numeriske kolonner for bedre lesbarhet
    display_formatted = display_df.copy()

    # Prosentkolonner
    if 'expected_annual_return' in display_formatted:
        display_formatted['exp_ret_%'] = (display_formatted['expected_annual_return'] * 100).round(0).astype(int)
        display_formatted = display_formatted.drop('expected_annual_return', axis=1)

    if 'breakeven_drop_pct' in display_formatted:
        display_formatted['be_drop_%'] = (display_formatted['breakeven_drop_pct'] * 100).round(0).astype(int)
        display_formatted = display_formatted.drop('breakeven_drop_pct', axis=1)

    # Tid til utløp i dager
    if 'timeToMaturity' in display_formatted:
        display_formatted['days'] = (display_formatted['timeToMaturity'] * 365).round(0).astype(int)
        display_formatted = display_formatted.drop('timeToMaturity', axis=1)

    # Rund av scores
    for col in ['taylor_score', 'originalScore', 'iv_rank']:
        if col in display_formatted:
            display_formatted[col] = display_formatted[col].round(0).astype(int)

    # Rund av ratios til 2 desimaler
    if 'price_vs_taylor_max' in display_formatted:
        display_formatted['prc/max'] = display_formatted['price_vs_taylor_max'].round(2)
        display_formatted = display_formatted.drop('price_vs_taylor_max', axis=1)

    # Forenkle kolonnenavn
    rename_map = {
        'underlyingSymbol': 'TICKER',
        'taylor_score': 'T_SCORE',
        'quality': 'QUAL',
        'originalScore': 'F_SCORE',
        'contractSymbol': 'CONTRACT',
        'lastPrice': 'PRICE',
        'taylor_max_price': 'MAX_PRC',
        'strike': 'STRIKE',
        'stockPrice': 'STOCK',
        'expirationDate': 'EXPIRY',
        'iv_rank': 'IV_RNK',
        'breakeven_price': 'BREAKEVEN',
        'bid': 'BID',
        'ask': 'ASK',
        'volume': 'VOL',
        'openInterest': 'OI'
    }

    display_formatted = display_formatted.rename(columns=rename_map)

    # Velg kolonner i best rekkefølge
    priority_cols = [
        'TICKER', 'CONTRACT', 'T_SCORE', 'QUAL', 'F_SCORE', 'prc/max',
        'PRICE', 'MAX_PRC', 'STRIKE', 'STOCK', 'EXPIRY', 'days', 'IV_RNK',
        'exp_ret_%', 'be_drop_%', 'BREAKEVEN', 'BID', 'ASK', 'VOL', 'OI'
    ]

    final_cols = [c for c in priority_cols if c in display_formatted.columns]
    display_formatted = display_formatted[final_cols]

    print(display_formatted.to_string(index=False, max_rows=DISPLAY_TOP_N))

    # Oppsummering
    print("\n" + "="*80)
    print("📊 OPPSUMMERING")
    print("="*80)
    print(f"• Antall aksjer analysert: {len(tickers)}")
    print(f"• Aksjer med kvalifiserte opsjoner: {len(results)}")
    print(f"• Totalt kvalifiserte opsjoner: {len(all_puts)}")
    print(f"• Gjennomsnittlig Taylor Score: {all_puts['taylor_score'].mean():.1f}")
    print(f"• Gjennomsnittlig pris: ${all_puts['lastPrice'].mean():.2f}")
    print(f"• Gjennomsnittlig forventet årlig avkastning: {all_puts['expected_annual_return'].mean()*100:.1f}%")

    # Topp 5 aksjer
    top_stocks = (
        all_puts.groupby('underlyingSymbol')
        .agg({
            'taylor_score': 'max',
            'lastPrice': 'mean',
            'expected_annual_return': 'mean'
        })
        .sort_values('taylor_score', ascending=False)
        .head(5)
    )

    print("\n🏆 TOPP 5 AKSJER:")
    for symbol, row in top_stocks.iterrows():
        print(f"   {symbol:6s} - Score: {row['taylor_score']:.1f}, "
              f"Avg pris: ${row['lastPrice']:.2f}, "
              f"Forventet avkastning: {row['expected_annual_return']*100:.0f}%")

    print("\n" + "="*80)
    print("💡 NESTE STEG:")
    print("="*80)
    print("1. Gjennomgü opsjonene over - sorter pü T_SCORE (høyere = bedre)")
    print("2. Fokuser pĂĽ: prc/max < 0.7, IV_RNK < 40, exp_ret_% > 60")
    print("3. Sjekk bid-ask spread: (ASK-BID)/PRICE bør vÌre <25%")
    print("4. Bruk GTC (Good-Til-Canceled) limit orders for ĂĽ fĂĽ bedre pris")
    print("   Eksempel: Hvis PRICE=$15.50, sett GTC limit=$14.00")
    print("5. Diversifiser: 12+ ulike aksjer, flere utløpsdatoer")
    print("6. Start med liten posisjon (1-5% av portefølje)")
    print("7. Hold til uken før utløp (Taylor-metoden)")
    print("\n⚠️  FORVENT: 50-80% expire worthless, men vinnerne vinner stort!")
    print("="*80 + "\n")


# ============================================================================
# INPUT DATA
# ============================================================================

if __name__ == "__main__":

    # Din score-liste (høyere score = dürligere aksje)
    score_str = """
FDMT	99.97
SKYH	99.94
PCT	99.91
FIP	99.88
BTDR	99.85
ADUR	99.82
APLD	99.79
CLSK	99.76
WULF	99.74
MESO	99.71
FBYD	99.68
BKSY	99.65
RWT	99.62
AXG	99.59
CIFR	99.56
CTEV	99.53
MSTR	99.50
RUN	99.47
ENVX	99.44
ANGX	99.41
CD	99.38
PLSE	99.35
PGEN	99.32
...
FRGE	97.06
"""

    # Kjør analysen
    main(score_str)```

From my perspective, I’ve been developing hedging triggers as well. However, these triggers are confirmatory rather than predictive—they activate once a crisis is already underway. Earlier attempts to detect crises in advance generated too many false positives, which ultimately hurt hedge performance. For this reason, I believe any effective trigger must be truly universal.I recently posted on X a thought inspired by Damodaran’s latest note:

“Correlations between global markets have risen sharply (especially in crises), so there is ‘no place to hide.’ Almost every company now has meaningful exposure to country risk, regardless of where it is headquartered.”

Damodaran’s core argument is that country risk can no longer be ignored or diversified away. Globalization has made companies and investors heavily exposed through foreign revenues, supply chains, and operations.One might assume that small-cap companies—with limited international exposure—would be somewhat insulated. In practice, however, when the tsunami begins in the large caps, small caps are usually swept away with everything else.At this stage, I would be very satisfied simply with a more consistent and reliable way to manage core risk parameters: controlling drawdowns, keeping standard deviation (volatility) low, and improving downside risk metrics such as the Sortino ratio.For a more structural and permanent hedge, something like a TZA-based approach or protective puts could make sense—though, as always, there is no free lunch.

Moreover, trying to build more sophisticated strategies to anticipate triggers risks falling into our worst nightmare: overfitting. Because genuine crises are extremely rare— can realistically be counted in any meaningful historical sample ?—it is far easier to overfit a model in this setting than it would be when developing a conventional trading strategy.

3 Likes

I don't want to die on the "timing works vs. doesn't work" hill, but I will say that timing as systematic risk transformation has served me well.

Individual signals can be fragile and overfit, so I work hard to only use those I trust, that make rational sense, and that have a backtestable history (for whatever that is or isn't worth).

I, too, use logic from various tactical ETF strategies — found in research papers, further vetted by sources like Allocate Smartly (love that site), etc.

I then work to combine signals so no single one overpowers the rest, and to modulate/graduate them — I don't want to be "too" risk-off, nor do I want my exposure changing starkly or often.

To keep friction and transaction costs down, I typically enact the initial 45% of any “risk-off” calls (which is where most of the whipsaws seem to be) through shorting futures.

I also trade a tranche of my models paired with a short futures position.

The way I view all this: I'm running numerous versions of my core underlying strategies that don't share perfect correlation with one another, each with a unique risk profile.

Finally, I run a deep-out-of-the-money, very near-term SPX puts hedge for crash protection. If I were ever forced to keep just one hedge, it would probably be this one — though in fairness, it really only pays on fast crashes. The slow grinds are what the timing layer is for. Different failure speeds, different tools.

The general premise of the puts: you pay an ongoing tax (I aim for ~3% per year) in good years for a fantastic payoff during true crises. The usual pushback is, "Sure, but other than say GFC or Covid, when would you even need it?" To me, that IS the point. Look at the models most of us trade–that’s when you would want the payoff. Once a decade is plenty.

The other aspect is that by running protected, I can, in theory, hold more equities than I could otherwise justify — and, crucially, get a nice influx of cash precisely when stocks go on sale. Looked at holistically, it stops feeling like the drag it appears to be and more like an offensive weapon.

There are also brief windows of extreme volatility where, if you pay attention and manage carefully, you can lock in a year of "free" protection here or there.

I'm not a pro, but this is what I do.

3 Likes

Maybe it’s a rotation instead of a top? Sure looks like it

It’s a very focalized sell off in terms of number of stocks, not capitalization. Big names that have run up and are now being sold off which takes the indexes down.

My shiny new strategy based on Sales Fitted Price Formula (and a few other filters) is up 2.7% today. INTU + 5%, EXLS +5%, PAYC +5%, etc.

But my cash account is down 1.8% bc it holds names like NVDA which I don’t want to sell or I’ll have a huge tax bill this year. I might hedge it. Not sure.

Cheers.

2 Likes

I meant to mention, the indicators I use aren’t too risk-averse now/yet. Only CLI, which is always a bit of an oddball.

I believe the best hedging is a good short-long (LS) strategy. Perhaps the simplest approach is, instead of trying to predict when the market will fall, to assess whether to slightly reduce or increase your short exposure, thereby slightly increasing your net long exposure. This way, you reduce the impact of “bad trades” and minimize the impact of turnover in your strategies.

What you’ll need to keep in mind is that, depending on the factors in your portfolio, some “risk on/off” signals might not be appropriate… It’s all about trial and error.

Some factors, such as Momentum or Low Vol, tend to perform terribly after sharp corrections. Others, such as Value and Size, tend to strengthen… Example: Factor Equity Curves

2 Likes

I've been back and forth about this for a long time. What I'm going to say now reflects my current beliefs, but I've been wrong before, and I'm sure I'll be wrong again.

I don't believe it's possible to time the market. See Market Timing, Tactical Asset Allocation, and Trading: A Dialogue.

That said, should one hedge?

If you're trading your own money and you're not using leverage, no. Ride the ups and downs. All hedges are expensive. You may lose 50% of your money at some point, but you're going to make it all back and then some if you're patient and don't panic and just stick to your guns.

Even if you're using a moderate amount of leverage and/or some options strategies that correlate with longs, thus increasing your long exposure to, say, 1.15X, don't hedge.

If you don't hedge, though, diversify. Canada is a great place to diversify if you're unable to handle the quirks and expenses of trading in Europe.

If you're using substantial leverage or if you're handling other people's money, then, and only then, apply a constant hedge.

The easiest and cheapest way to hedge is to go short an ETF that correlates well with your longs, that has no borrow fee (and, ideally, offers a short rebate), and that isn't going to be recalled during a downturn. You can also short an index directly. The choice of ETF/index matters: don't short something too volatile, and try to find an ETF with some sort of structural flaw. I chose an index whose construction I have real problems with and consider a long-term money-loser.

I found that using puts and shorting small caps and microcaps were very unhealthy for my portfolio. They are extraordinarily expensive ways to hedge compared to ETFs.

However, I now supplement the ETF I short with 20 equally weighted stocks, chosen carefully, with strict rules about sector exposure, all of them with high rebates and no borrowing fees and plenty of liquidity. Since I implemented this combination on May 21, my shorts have made me 10%.

With short positions, it's important to rebalance regularly (every two weeks) to keep them a more-or-less constant portion of your portfolio. This means you'll be feeding yourself cash for your longs during a downturn, which is a nice thing to do.

9 Likes

I am very fascinated...

  • I assume you are using several short ETFs to achieve this?
  • Have you backtested the solution, and what did that backtest look like?
  • Do you use leveraged products, i.e., short x2 or x3?
  • Which ETFs are you currently using? And I assume you continuously assess whether this or these need to be replaced with others?
  • What site do you use to research and find the right ETF? I've looked at P123's solution and screened for CoName("short"), and Yahoo's, but some information is missing.

I hope you write an article about exactly this. It was very interesting ...

In continuation of our talk here, these are the interesting points:

10-Year Stock Market Return Forecast - Allocate Smartly

Margin Debt Is at an All-Time High, What Does That Mean? - Allocate Smartly

Here is a link to a prior discussion on this where I explain using TZA

1 Like

I used this for a while. TZA has the advantage of having a large inverse correlation to at least one of my models. For a SEP-IRA it is a way of getting some leverage into the account.

On backtesting at least, this inverse correlation did what one would want it to: decrease the beta of the portfolio while maintaining much of the alpha.

Volatility drag of an 3X inverse ETF is not a problem if it is rebalanced frequently. In fact, in theory one could get some volatility harvesting. Sadly, I believe any effect from volatility harvesting is probably very small. But at least there is no volatility drag with frequent rebalancing.

Thanks for the idea Charles.

1 Like

No, just one ETF at the moment. The backtests look fine. The hedge does not make money but it smooths returns and helps the longs. I don't use leveraged products, and I don't want to give away the ETF I'm using, but it is one of the P123 benchmark ETFs. As far as research, I did the research years ago when I was investigating indexes, and my impression stuck with me. The ETF isn't a "short ETF," it's a regular index ETF that I sell short.

1 Like

I generally agree that most hedging is "expensive," and honestly, I wish I did less of it. Even so, I don't think the decision to hedge can be reduced to a single binary of whether you're managing your own money or not.

Are you in the accumulation phase? Are you living off your assets? For someone drawing on a portfolio, sequence-of-returns risk changes the math. A drawdown you're forced to sell into does damage that later recoveries never fully repair.

I even hedge a small retirement account that I know I shouldn't. If I left it unhedged and it started outrunning my main account, I'd be tempted to strip the hedges off the account that actually matters. I know that's illogical and irrational. But I also know that I am illogical and irrational at times, and I'd rather make the cheap mistake (hedging the small account) than risk the expensive one (un-hedging the large one).

Finally, and maybe most importantly, it depends on what kind of hedging you're talking about. I'd split it into two types:

A) Hedging day-to-day volatility. I agree that most people, myself included, would be better off not doing this. I still do a lot of it, because I know my own personality. It's a challenge, and a lot of work, to do it as cost-effectively as possible, and it's still costly in the end. This is the hedging I wish I had the gumption to drop.

B) Black-swan hedging. I believe highly convex crash protection is chronically underpriced. Yes, it bleeds: day-to-day, week-to-week, month-to-month, year-to-year, sometimes even decade-to-decade. But taken holistically, I think it can be additive over time:

  • With that insurance layer over the portfolio, I lean harder into stocks than I otherwise would, holding less cash and fewer alternatives. Running hotter in the good times pays much of the premium.

  • In a true crisis, the layer explodes in value, cushioning the hit and providing an influx of cash to buy stocks at depressed prices.

Add it up (more equity exposure in normal times, a less dramatic crash experience, dry powder at the bottom) and I don't see B as a cost at all. I see it as additive.

Anyway, I wish I could give up A, and I agree with Yuval that I probably should. As for B, I have a feeling I'll take a tranche of fast-decaying SPX puts to my grave.

I've been thinking about this problem as well. My first approach was to build a dedicated short strategy using AI Factor, but in backtesting I couldn't find anything that outperformed a simple IWM hedge.

The biggest weakness of my small-cap strategies is their drawdown during black swan events. They behave broadly in line with IWM during both the 2008 financial crisis and the COVID sell-off, but a 60% maximum drawdown still leaves me concerned about the risk of ruin. Ideally, I'd like to cap drawdowns closer to 50%.

One idea I've been exploring is buying 1-year IWM put options with strikes around 30% below the current market price. Even if my portfolio were to underperform IWM by 10% during a severe sell-off, this should still provide protection from total blow up.

The main drawback is liquidity. Deep out-of-the-money IWM puts often have bid-ask spreads exceeding 10%, making them expensive to implement in practice.

In my backtests, hedging with IWM reduced annual return by around 6%, whereas continuously purchasing deep OTM puts appears likely to cost closer to 2% per year.

I'd be very interested to hear from anyone who has implemented a similar tail-risk hedge. Has anyone found a more cost-effective approach?

XLK has been pretty volatile for the last month and is down a little:

I have it as 61% chance this is just a "state" of sideways volatility (labelled recovery in this chart) based on a Hidden Markov Model. Hmmlearn uses unsupervised clustering in this 3 state model to identify states— which makes it unbiased at least:

So I don't think this model has a high level of accuracy. lt does have a low enough false positive rate for the crisis state to avoid whip-saws and make it useful for getting out of a significant number of drawdowns (not all).

So 61% chance AI stocks are fine but the risk is not trivial (with relatively low accuracy to be clear).

You can find a basic HMM app here (Google AI Studio). I believe you can fork it using Remix. This is basic in that it just uses pricing data. You may want to add your own time-series data which could include some FED data, spreads, or the VIX for example: Basic HMM using only pricing data

The data are called "emissions" that are not hidden. The idea is that Mr. Market is in a state, of possibly crisis as an example, but he will never tell you that and this is therefore hidden in a Hidden Markov Model. HMMs try to determine what state Mr. Market is in using the emissions and some Bayesian statistics to give you a likelihood (or posterior probability) for each state.

1 Like

If it’s truly for crash protection, as mine is, it doesn’t need to match your book.

SPX is far more liquid and when everything hits the fan… All I need is something convex that goes up a lot.

In that regard, 1 year is much too far out for my taste. I want something very near-term that I am consistently rolling forward, almost always at a significant loss.

I aim to lose 0.25% per month in most conditions.

By paying attention, I find occasional, random non-crash vix spikes that can at times defray my costs, sometimes substantially.

SPX options also get 1256 tax treatment and cash settlement, IWM options do not.

One caveat: this is my tool for crashes; grinds need a different tool.

2 Likes

One thing I think is not accounted for that runs tangential to the hedging discussion is how inadvertently clustered my portfolio gets into certain themes. Even when I put hard sector/industry weighting caps on my buy and sell rules, I can still often find myself over exposed to one trade. For example, I’m assuming many of us are heavily into both Shipping and Oil and Gas Services, which is essentially the same Iran War trade and mostly move together. My portfolio yo-yos up and down based on the news of the day on that particular macro theme. I’m not sure there’s a good quantitative way to account for this, and I find myself getting more and more drawn to utilizing the best LLMs for qualitative assements of portfolio construction and buy/sell candidates for the “last mile” and using my system to get me ~97% there. If you’re just blindly buying the highest ranked stock on a weekly rebalance, is that stock actually adding anything accreditive to your overall portfolio are are you adding the 6th best stock to a theme you already have 5 current holdings for? But of course there is no way to backtest any qualitative extra seasoning.

An allocation of say 50% QMHIX, 30% GLD and 20% EDV is appealing to me as more permanent diversifier.

I have the same problem. For example, my short book can be spread widely among various sectors, but the stocks it chooses can all be uranium-related, since you can find uranium-related stocks in materials, industrials, energy, and utilities.

There is: use price correlation. Stocks that go up and down over the last year based on the same externalities will show strong price correlation. The P123 command for live and simulated strategies is MaxCorrel. www.portfolio123.com/doc/doc_detail.jsp?factor=MaxCorrel I haven't tried this myself, but it appears promising. It's not clear from the documentation whether it measures the correlation to each of the individual holdings and takes the maximum, or whether it measures the correlation to the portfolio as a whole.

2 Likes

For pairs trading and stat-arb, cointegration is often used in addition to correlation.

While correlation measures short-term return co-movement (which can drift apart indefinitely), cointegration tests whether a linear combination of two asset prices forms a stationary, mean-reverting spread. Cointegrated assets move together long-term, so when their price spread widens, it creates an entry point for mean-reversion trades.

For anyone testing this, Portfolio Visualizer has a free cointegration tool: Portfolio Visualizer Cointegration. It's also straightforward to set up in Python using statsmodels (or with AI assistance). Claude or Google's Antigravity can get members set up if they want to try this.

It really is essential for pairs trading at least. It is always used for pairs-trading, professionally. Also as part of a workflow, I have seen a dendrogram (hierarchical clustering) used as a screen to find candidates for pairs with both correlation and cointegration (in an online Coursera course).

From Portfolio Visualizer:

"This tool allows assets to be tested for cointegration using the Augmented Dickey-Fuller Test. Cointegrated assets typically have a financial or economic relationship that prevents them from diverging and the price difference tends to be mean reverting."