---
# System prepended metadata

title: Sentiment-Driven Trading with FinBERT (AAPL)

---

# Sentiment-Driven Trading with FinBERT (AAPL)

## Thesis Statement

This strategy tests whether sentiment from real-time financial news can lead to alpha (i.e. profitable, risk-adjusted trading strategy). Specifically, we leverage FinBERT—a BERT-based NLP model trained on financial texts—to quantify the tone of Apple-related news in minute-level resolution, then use this sentiment signal to decide short-term trades. 

While FinBERT alone doesn’t guarantee profitability, we're looking to bridge the gap between sentiment and execution by designing a trading logic that uses sentiment shifts to make smart trades.

## Strategy Design

- Data source: Tiingo minute-level news on AAPL + FinBERT sentiment scores
- Model logic: Signal-based long/short execution with risk controls
- Validation: Backtested on 5/10–5/16, deployed on 5/17–5/20
- Risk filters: Max drawdown <15%, Sharpe Ratio >1, position weight >25%

## Assumptions

- Sentiment timing assumes fast market reaction—delayed headlines weaken edge
- Noise in minute-level sentiment may trigger false positives; we apply filters
- Strategy could underperform during news-sparse days or if headlines are contradictory

We backtested multiple variants: e.g., weighting trades by sentiment strength, filtering on momentum, and combining with moving average trend confirmation.

## Strategy - Technical Indicators

1. **Exponential Moving Averages (EMA)**
	- Fast EMA: 5-minute
	- Slow EMA: 15-minute
	- Signal Logic:
	    - Bullish: EMA(5) > EMA(15)
	    - Bearish: EMA(5) < EMA(15)

2. **Relative Strength Index (RSI)**
	- RSI Period: 9 (SMA-based)
	- Signal Logic:
	    - Buy only when RSI < 70 (avoid entering trades when the stock is overbought)
	    - Sell only when RSI > 30 (avoid exiting trades when the stock is oversold)

## Strategy - FinBERT Sentiment Analysis

- Model: ProsusAI/FinBERT
- Data Source: Tiingo news descriptions mentioning AAPL
- Sentiment Score = P(Positive) - P(Negative) (range: -1 to 1)
- Rolling Window: 5 most recent sentiment scores

### Signal Threshold Logic

- Uses dynamic threshold = 0.7 × Std Dev of recent scores
- Signal Actions:
    - Strong Positive (> threshold): Full Long
    - Mild Positive (0.25 to threshold): Half Long
    - Neutral (-0.25 to 0.25): No Position
    - Mild Negative (-threshold to -0.25): Half Short
    - Strong Negative (< -threshold): Full Short

*This avoids overreacting to minor sentiment swings and smooths false signals*

## Trade Execution Logic

### **AAPL Stock**

- Go Long when:
    - Sentiment > threshold
    - EMA(5) > EMA(15)
    - RSI < 70
- Go Short when:
    - Sentiment < -threshold
    - EMA(5) < EMA(15)
    - RSI > 30
- Exposure bounds:
    - Min: 25%
    - Max: 80%

### **Options (Directional + Spreads)**

- Buy Calls
    - Sentiment > threshold
    - EMA + RSI bullish
    - IV < 90% of Historical Vol
- Buy Puts
    - Sentiment < -threshold
    - EMA + RSI bearish
    - IV < 90% of Historical Vol
- Sell Bear Call Spread
    - Sentiment < -threshold
    - EMA + RSI bearish
    - IV > 110% of HV
- Sell Bull Put Spread
    - Sentiment > threshold
    - EMA + RSI bullish
    - IV > 110% of HV

## Risk Management

- Stop Loss: 5%
- Take Profit: 8%
- Max Drawdown Limit: 15%
    - If breached: cut all positions by 50%
- Portfolio Exposure: 25–80% at all times

## Delta Hedging (Options Only)

- Reduce net directional exposure from options
- Net Delta = ∑(option delta × contract size × 100)
- Hedge with AAPL stock when drift > 10 shares
- Hedging frequency: Every 5 minutes

## Rebalancing & Scheduling

- Sentiment + indicators: Every minute
- Delta hedging + risk management: Every 5 minutes
- HV updates + logging: Daily after market open
- Options positions closed: 10 minutes before market close

## Backtest Result (10-Day Return)

![image](https://hackmd.io/_uploads/BJxhHpWgeg.png)
![image](https://hackmd.io/_uploads/SyfyITWggg.png)


## Bottom Line

Our strategy combines news sentiment with technical signals to trade AAPL in both stock and options. When headlines suggest strong positive or negative sentiment, we first cross-check with momentum (EMA) and strength (RSI) to confirm the trend. If all signals align, we take a position sized to our confidence level.

To manage risk in real time, we use stop-losses and take-profits to cap potential losses and lock in gains. We use Delta hedging in the options layer to neutralize unintended exposure when the market moves. We also enforce strict position size rules to avoid overexposure.

What's unique about our strategy is the use of a dynamic sentiment threshold—it adapts based on recent volatility in sentiment scores. This helps us ignore minor mood swings in the news and act only when the signal is strong and meaningful. 

## QuantConnect Backtest Algorithm

```
from AlgorithmImports import *
import tensorflow as tf
from transformers import TFBertForSequenceClassification, BertTokenizer
import numpy as np
import pandas as pd
from datetime import timedelta, datetime

class EnhancedFinBERTStrategy(QCAlgorithm):
    def Initialize(self) -> None:
        self.SetStartDate(2023, 5, 10) 
        self.SetEndDate(2023, 5, 20)
        self.SetCash(1000000)      

        self.aapl = self.AddEquity("AAPL", Resolution.Minute)
        self.aapl_symbol = self.aapl.Symbol
        
        self.aapl_option = self.AddOption("AAPL", Resolution.Minute)
        self.aapl_option.SetFilter(self.OptionFilterFunction)
        
        self.tiingo_symbol = self.AddData(TiingoNews, self.aapl_symbol).Symbol

        self.model_name = "ProsusAI/finbert"
        self.tokenizer = BertTokenizer.from_pretrained(self.model_name)
        self.model = TFBertForSequenceClassification.from_pretrained(self.model_name, from_pt=True)

        self.sentiment_window = []
        self.sentiment_window_size = 5      
        self.sentiment_threshold_base = 0.4  
        self.dynamic_threshold = self.sentiment_threshold_base
        self.dynamic_threshold_factor = 0.7

        self.trades_today = 0
        self.min_trades_per_day = 2
        self.max_trades_per_day = 5
        self.daily_threshold_adjustment = 0.05  

       
        self.sentiment_cooldown = 5        
        self.last_sentiment_trade = datetime.min
        
        self.current_holdings = 0
        self.target_holdings = 0
        self.max_position_size = 0.8
        self.min_position_size = 0.5
        
        self.active_option_positions = {}
        self.last_hedge_time = datetime.min
        self.hedge_frequency = timedelta(minutes=5)
        self.delta_hedge_threshold = 0.1
        
        self.max_drawdown = 0.15
        self.initial_portfolio_value = self.Portfolio.TotalPortfolioValue
        self.highest_portfolio_value = self.Portfolio.TotalPortfolioValue
        self.stop_loss_percentage = 0.05
        self.profit_target = 0.08
        self.entry_price = 0
        
        self.rsi = self.RSI(self.aapl_symbol, 9, MovingAverageType.Simple, Resolution.Minute)
        self.ema_fast = self.EMA(self.aapl_symbol, 5, Resolution.Minute)
        self.ema_slow = self.EMA(self.aapl_symbol, 15, Resolution.Minute)
        
        self.historical_volatility = 0
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen("AAPL", 5), self.UpdateHistoricalVolatility)
        
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(TimeSpan.FromMinutes(5)), self.ManageRisk)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.Every(TimeSpan.FromMinutes(3)), self.PerformDeltaHedging)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose("AAPL", 10), self.EndOfDayPositionManagement)
        
        self.is_training_period = True
        self.train_end_date = datetime(2023, 5, 16)
        
        self.sentiment_stats = {
            "positive_count": 0,
            "negative_count": 0,
            "neutral_count": 0,
            "total_count": 0
        }
        
        self.daily_returns = []
        self.previous_day_value = self.Portfolio.TotalPortfolioValue
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen("AAPL", 1), self.RecordDailyMetrics)

        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketClose("AAPL", 0), self.AdaptiveThresholdAdjustment)

        self.sharpe_ratio = 0
        
        self.SetBenchmark(self.aapl_symbol)
        
        self.Debug(f"Algorithm initialized: {self.Time}")

    def OptionFilterFunction(self, universe):
        return universe.Expiration(timedelta(days=7), timedelta(days=14)).Strikes(-5, 5)
    
    def UpdateHistoricalVolatility(self):
        history = self.History(self.aapl_symbol, 30, Resolution.Daily)
        
        if not history.empty and len(history) >= 21:
            closes = history['close'].values[-21:]
            log_returns = np.diff(np.log(closes))
            self.historical_volatility = np.std(log_returns) * np.sqrt(252)
            self.Debug(f"Updated historical volatility: {self.historical_volatility:.4f}")
            
            self.daily_pnl_start = self.Portfolio.TotalPortfolioValue

    def OnData(self, slice: Slice) -> None:
        if self.Time.date() > self.train_end_date.date():
            self.is_training_period = False
        
        if slice.ContainsKey(self.tiingo_symbol):
            news = slice[self.tiingo_symbol]
            
            if not news.description:
                return
                
            sentiment_score = self.AnalyzeSentiment(news.description)
            self.sentiment_window.append(sentiment_score)
            
            if len(self.sentiment_window) > self.sentiment_window_size:
                self.sentiment_window.pop(0)
            
            self.UpdateDynamicThreshold()
            
            if abs(sentiment_score) > 0.3:
                sentiment_type = "POSITIVE" if sentiment_score > 0 else "NEGATIVE"
                self.Log(f"{self.Time} - {sentiment_type} NEWS: {news.title[:100]}... (Score: {sentiment_score:.2f})")
            
            self.sentiment_stats["total_count"] += 1
            if sentiment_score > 0:
                self.sentiment_stats["positive_count"] += 1
            elif sentiment_score < 0:
                self.sentiment_stats["negative_count"] += 1
            else:
                self.sentiment_stats["neutral_count"] += 1
            
            self.GenerateTradingSignals()
        
        if slice.OptionChains.Count > 0:
            chain = slice.OptionChains.get(self.aapl_symbol)
            if chain is not None:
                self.UpdateOptionDeltas(chain)
                
                if abs(self.target_holdings - self.current_holdings) > 0.2:
                    time_since_last_trade = (self.Time - self.last_sentiment_trade).total_seconds() / 60
                    if time_since_last_trade > self.sentiment_cooldown:
                        self.ExecuteOptionsStrategy(chain)

    def AnalyzeSentiment(self, text):
        max_length = 512
        text = text[:2000]  
        
        inputs = self.tokenizer(text, return_tensors="tf", max_length=max_length, truncation=True)
        outputs = self.model(**inputs)
        
        logits = outputs.logits
        probabilities = tf.nn.softmax(logits, axis=-1).numpy()[0]
        
        sentiment_score = probabilities[2] - probabilities[0]  
        
        return sentiment_score

    def UpdateDynamicThreshold(self):
        if len(self.sentiment_window) >= 3:
            std_dev = np.std(self.sentiment_window)
            self.dynamic_threshold = max(0.05, self.dynamic_threshold_factor * std_dev)
        else:
            self.dynamic_threshold = self.sentiment_threshold_base

    def GenerateTradingSignals(self):
        if len(self.sentiment_window) < 3:
            return
            
        time_since_last_trade = (self.Time - self.last_sentiment_trade).total_seconds() / 60
        if time_since_last_trade < self.sentiment_cooldown:
            return
        
        avg_sentiment = sum(self.sentiment_window) / len(self.sentiment_window)
        
        if not self.rsi.IsReady or not self.ema_fast.IsReady or not self.ema_slow.IsReady:
            return
        
        rsi_value = self.rsi.Current.Value
        ema_signal = 1 if self.ema_fast.Current.Value > self.ema_slow.Current.Value else -1
        
        signal_strength = 0
        
        if avg_sentiment > self.dynamic_threshold and rsi_value < 70 and ema_signal == 1:
            signal_strength = 1
        elif avg_sentiment < -self.dynamic_threshold and rsi_value > 30 and ema_signal == -1:
            signal_strength = -1
        else:
            if avg_sentiment > 0.25:
                signal_strength = 0.5
            elif avg_sentiment < -0.25:
                signal_strength = -0.5
        
        if abs(signal_strength) >= 0.5:
            self.target_holdings = signal_strength
            if abs(self.current_holdings - self.target_holdings) > 0.2:
                self.ExecuteStockTrades()

    def ExecuteStockTrades(self):
        target_pct = max(min(abs(self.target_holdings) * self.max_position_size, self.max_position_size), self.min_position_size)
        target_pct = target_pct * (1 if self.target_holdings > 0 else -1)
        
        current_drawdown = (self.highest_portfolio_value - self.Portfolio.TotalPortfolioValue) / self.highest_portfolio_value
        if current_drawdown > self.max_drawdown:
            self.Log(f"Maximum drawdown limit reached: {current_drawdown:.2%}. Trade not executed.")
            return
            
        self.SetHoldings(self.aapl_symbol, target_pct)
        self.current_holdings = self.target_holdings
        self.entry_price = self.Securities[self.aapl_symbol].Price
        self.last_sentiment_trade = self.Time
        
        self.trades_today += 1
        
        self.Log(f"Trade executed: Set AAPL holdings to {target_pct:.2%} of portfolio. Entry price: ${self.entry_price}")

    def ExecuteOptionsStrategy(self, chain):
        """Execute options strategy based on sentiment signal and implied volatility"""
        underlying_price = self.Securities[self.aapl_symbol].Price
        
        atm_options = [x for x in chain if abs(x.Strike - underlying_price) < 2]
        if not atm_options:
            return
            
        atm_options.sort(key=lambda x: abs(x.Strike - underlying_price))
        
        if self.target_holdings > 0:
            call_options = [x for x in atm_options if x.Right == OptionRight.Call]
            if not call_options:
                return
                
            selected_call = call_options[0]
            implied_vol = selected_call.ImpliedVolatility
            
            if implied_vol < self.historical_volatility * 0.9:
                option_allocation = self.Portfolio.Cash * 0.1
                quantity = max(1, int(option_allocation / (selected_call.AskPrice * 100)))
                
                self.MarketOrder(selected_call.Symbol, quantity)
                self.Log(f"LONG CALL: Bought {quantity} {selected_call.Symbol} at strike ${selected_call.Strike}")
                
                self.active_option_positions[selected_call.Symbol] = {
                    "position": quantity,
                    "delta": selected_call.Greeks.Delta,
                    "gamma": selected_call.Greeks.Gamma,
                    "entry_price": selected_call.AskPrice
                }
                self.trades_today += 1

            elif implied_vol > self.historical_volatility * 1.1:
                sell_call = call_options[0]
                otm_calls = [x for x in chain if x.Right == OptionRight.Call and x.Strike > sell_call.Strike + 5]
                if not otm_calls:
                    return
                otm_calls.sort(key=lambda x: x.Strike)
                buy_call = otm_calls[0]
                
                spread_credit = sell_call.BidPrice - buy_call.AskPrice
                if spread_credit <= 0:
                    return
                
                max_risk = (buy_call.Strike - sell_call.Strike - spread_credit) * 100
                option_allocation = self.Portfolio.Cash * 0.05
                quantity = max(1, int(option_allocation / max_risk))
                
                self.Sell(sell_call.Symbol, quantity)
                self.Buy(buy_call.Symbol, quantity)
                
                self.Log(f"BEAR CALL SPREAD: Sold {quantity} {sell_call.Symbol} at ${sell_call.Strike}, " +
                         f"Bought {quantity} {buy_call.Symbol} at ${buy_call.Strike}")
                
                self.active_option_positions[sell_call.Symbol] = {
                    "position": -quantity,
                    "delta": -sell_call.Greeks.Delta,
                    "gamma": -sell_call.Greeks.Gamma,
                    "entry_price": sell_call.BidPrice
                }
                self.active_option_positions[buy_call.Symbol] = {
                    "position": quantity,
                    "delta": buy_call.Greeks.Delta,
                    "gamma": buy_call.Greeks.Gamma,
                    "entry_price": buy_call.AskPrice
                }
                self.trades_today += 1

        elif self.target_holdings < 0:
            put_options = [x for x in atm_options if x.Right == OptionRight.Put]
            if not put_options:
                return
                
            selected_put = put_options[0]
            implied_vol = selected_put.ImpliedVolatility
            
            if implied_vol < self.historical_volatility * 0.9:
                option_allocation = self.Portfolio.Cash * 0.1
                quantity = max(1, int(option_allocation / (selected_put.AskPrice * 100)))
                
                self.MarketOrder(selected_put.Symbol, quantity)
                self.Log(f"LONG PUT: Bought {quantity} {selected_put.Symbol} at strike ${selected_put.Strike}")
                
                self.active_option_positions[selected_put.Symbol] = {
                    "position": quantity,
                    "delta": selected_put.Greeks.Delta,
                    "gamma": selected_put.Greeks.Gamma,
                    "entry_price": selected_put.AskPrice
                }
                self.trades_today += 1

            elif implied_vol > self.historical_volatility * 1.1:
                sell_put = put_options[0]
                otm_puts = [x for x in chain if x.Right == OptionRight.Put and x.Strike < sell_put.Strike - 5]
                if not otm_puts:
                    return
                otm_puts.sort(key=lambda x: -x.Strike)
                buy_put = otm_puts[0]
                
                spread_credit = sell_put.BidPrice - buy_put.AskPrice
                if spread_credit <= 0:
                    return
                
                max_risk = (sell_put.Strike - buy_put.Strike - spread_credit) * 100
                option_allocation = self.Portfolio.Cash * 0.05
                quantity = max(1, int(option_allocation / max_risk))
                
                self.Sell(sell_put.Symbol, quantity)
                self.Buy(buy_put.Symbol, quantity)
                
                self.Log(f"BEAR PUT SPREAD: Sold {quantity} {sell_put.Symbol} at ${sell_put.Strike}, " +
                         f"Bought {quantity} {buy_put.Symbol} at ${buy_put.Strike}")
                
                self.active_option_positions[sell_put.Symbol] = {
                    "position": -quantity,
                    "delta": -sell_put.Greeks.Delta,
                    "gamma": -sell_put.Greeks.Gamma,
                    "entry_price": sell_put.BidPrice
                }
                self.active_option_positions[buy_put.Symbol] = {
                    "position": quantity,
                    "delta": buy_put.Greeks.Delta,
                    "gamma": buy_put.Greeks.Gamma,
                    "entry_price": buy_put.AskPrice
                }
                self.trades_today += 1

    def UpdateOptionDeltas(self, chain):
        positions_to_remove = []
        
        for symbol in self.active_option_positions:
            contract = next((c for c in chain if c.Symbol == symbol), None)
            
            if contract is not None:
                old_delta = self.active_option_positions[symbol]["delta"]
                new_delta = contract.Greeks.Delta
                
                position_size = self.active_option_positions[symbol]["position"]
                scaled_new_delta = new_delta * position_size
                
                self.active_option_positions[symbol]["delta"] = scaled_new_delta
                self.active_option_positions[symbol]["gamma"] = contract.Greeks.Gamma * position_size
                
                entry_price = self.active_option_positions[symbol]["entry_price"]
                current_price = contract.BidPrice if position_size > 0 else contract.AskPrice
                price_change = (current_price - entry_price) / entry_price
                
                if (position_size > 0 and price_change > self.profit_target) or \
                   (position_size > 0 and price_change < -self.stop_loss_percentage) or \
                   (position_size < 0 and -price_change > self.profit_target) or \
                   (position_size < 0 and -price_change < -self.stop_loss_percentage):
                    
                    if position_size > 0:
                        self.Sell(symbol, abs(position_size))
                        outcome = "profit" if price_change > 0 else "stop-loss"
                        self.Log(f"Closed LONG {symbol} for {price_change:.2%} {outcome}")
                    else:
                        self.Buy(symbol, abs(position_size))
                        outcome = "profit" if price_change < 0 else "stop-loss"
                        self.Log(f"Closed SHORT {symbol} for {-price_change:.2%} {outcome}")
                    
                    positions_to_remove.append(symbol)
            else:
                positions_to_remove.append(symbol)
        
        for symbol in positions_to_remove:
            if symbol in self.active_option_positions:
                del self.active_option_positions[symbol]

    def PerformDeltaHedging(self):
        if not self.active_option_positions or (self.Time - self.last_hedge_time) < self.hedge_frequency:
            return
            
        net_option_delta = sum(position["delta"] for position in self.active_option_positions.values())
        net_option_delta_shares = net_option_delta * 100
        
        current_stock_position = self.Portfolio[self.aapl_symbol].Quantity
        target_stock_position = -int(net_option_delta_shares)
        shares_to_trade = target_stock_position - current_stock_position
        
        if abs(shares_to_trade) > 10:
            underlying_price = self.Securities[self.aapl_symbol].Price
            hedge_cost = abs(shares_to_trade) * underlying_price
            
            if hedge_cost < self.Portfolio.Cash * 0.9:
                if shares_to_trade > 0:
                    self.Buy(self.aapl_symbol, abs(shares_to_trade))
                    self.Log(f"Delta Hedge: Bought {abs(shares_to_trade)} shares to offset option delta of {net_option_delta:.2f}")
                else:
                    self.Sell(self.aapl_symbol, abs(shares_to_trade))
                    self.Log(f"Delta Hedge: Sold {abs(shares_to_trade)} shares to offset option delta of {net_option_delta:.2f}")
                
                self.last_hedge_time = self.Time

    def ManageRisk(self):
        if not self.Portfolio.Invested:
            return
            
        if self.Portfolio.TotalPortfolioValue > self.highest_portfolio_value:
            self.highest_portfolio_value = self.Portfolio.TotalPortfolioValue
            
        current_drawdown = (self.highest_portfolio_value - self.Portfolio.TotalPortfolioValue) / self.highest_portfolio_value
        
        if current_drawdown > self.max_drawdown:
            self.Log(f"Maximum drawdown limit reached: {current_drawdown:.2%}. Reducing exposure.")
            new_target = self.current_holdings * 0.5
            self.SetHoldings(self.aapl_symbol, new_target)
            self.current_holdings = new_target
            
            for symbol, details in list(self.active_option_positions.items()):
                position_size = details["position"]
                reduction = abs(position_size) // 2
                
                if reduction > 0:
                    if position_size > 0:
                        self.Sell(symbol, reduction)
                        self.Log(f"Risk reduction: Sold {reduction} of {symbol}")
                        self.active_option_positions[symbol]["position"] -= reduction
                    else:
                        self.Buy(symbol, reduction)
                        self.Log(f"Risk reduction: Bought {reduction} of {symbol}")
                        self.active_option_positions[symbol]["position"] += reduction
            
            return
            
        if self.current_holdings != 0 and self.entry_price > 0:
            current_price = self.Securities[self.aapl_symbol].Price
            
            if self.current_holdings > 0:
                pnl_pct = (current_price - self.entry_price) / self.entry_price
                
                if pnl_pct < -self.stop_loss_percentage:
                    self.Log(f"Stop loss triggered: {pnl_pct:.2%}. Closing position.")
                    self.Liquidate(self.aapl_symbol)
                    self.current_holdings = 0
                    return
                    
                if pnl_pct > self.profit_target:
                    self.Log(f"Profit target reached: {pnl_pct:.2%}. Taking profits.")
                    self.Liquidate(self.aapl_symbol)
                    self.current_holdings = 0
                    return
                    
            elif self.current_holdings < 0:
                pnl_pct = (self.entry_price - current_price) / self.entry_price
                
                if pnl_pct < -self.stop_loss_percentage:
                    self.Log(f"Stop loss triggered: {pnl_pct:.2%}. Closing position.")
                    self.Liquidate(self.aapl_symbol)
                    self.current_holdings = 0
                    return
                    
                if pnl_pct > self.profit_target:
                    self.Log(f"Profit target reached: {pnl_pct:.2%}. Taking profits.")
                    self.Liquidate(self.aapl_symbol)
                    self.current_holdings = 0
                    return

    def EndOfDayPositionManagement(self):
        for symbol in list(self.active_option_positions.keys()):
            position_details = self.active_option_positions[symbol]
            position_size = position_details["position"]
            
            if position_size > 0:
                self.Sell(symbol, abs(position_size))
                self.Log(f"Closed end-of-day option position: {symbol}")
            elif position_size < 0:
                self.Buy(symbol, abs(position_size))
                self.Log(f"Closed end-of-day option position: {symbol}")
                
            del self.active_option_positions[symbol]
        
        if abs(self.current_holdings) > 0:
            overnight_holdings = self.current_holdings * 0.5
            self.SetHoldings(self.aapl_symbol, overnight_holdings)
            self.Log(f"Reduced overnight exposure to {overnight_holdings:.2%}")
            self.current_holdings = overnight_holdings

    def RecordDailyMetrics(self):
        daily_return = (self.Portfolio.TotalPortfolioValue / self.previous_day_value) - 1
        self.daily_returns.append(daily_return)
        
        self.previous_day_value = self.Portfolio.TotalPortfolioValue
        
        if len(self.daily_returns) > 1:
            returns_array = np.array(self.daily_returns)
            mean_return = np.mean(returns_array)
            return_stdev = np.std(returns_array)
            
            if return_stdev > 0:
                self.sharpe_ratio = (mean_return / return_stdev) * np.sqrt(252)
                
                self.Log(f"Daily Return: {daily_return:.2%}, Sharpe Ratio: {self.sharpe_ratio:.2f}")
                
                self.Plot("Performance", "Daily Return", daily_return * 100)
                self.Plot("Performance", "Sharpe Ratio", self.sharpe_ratio)
                self.Plot("Performance", "Drawdown", (self.highest_portfolio_value - self.Portfolio.TotalPortfolioValue) / self.highest_portfolio_value * 100)
                
                total = max(self.sentiment_stats["total_count"], 1)
                self.Plot("Sentiment", "Positive %", self.sentiment_stats["positive_count"] / total * 100)
                self.Plot("Sentiment", "Negative %", self.sentiment_stats["negative_count"] / total * 100)
                self.Plot("Sentiment", "Neutral %", self.sentiment_stats["neutral_count"] / total * 100)
                
                if self.active_option_positions:
                    net_delta = sum(pos["delta"] for pos in self.active_option_positions.values()) * 100
                    net_gamma = sum(pos["gamma"] for pos in self.active_option_positions.values()) * 100
                    self.Plot("Greeks", "Net Delta", net_delta)
                    self.Plot("Greeks", "Net Gamma", net_gamma)

    def AdaptiveThresholdAdjustment(self):
        if self.trades_today < self.min_trades_per_day:
            self.dynamic_threshold = max(0.05, self.dynamic_threshold - self.daily_threshold_adjustment)
            self.Log(f"[AdaptiveThresholdAdjustment] Trades today={self.trades_today} < {self.min_trades_per_day}, lower threshold to {self.dynamic_threshold:.2f}")
        elif self.trades_today > self.max_trades_per_day:
            self.dynamic_threshold += self.daily_threshold_adjustment
            self.Log(f"[AdaptiveThresholdAdjustment] Trades today={self.trades_today} > {self.max_trades_per_day}, raise threshold to {self.dynamic_threshold:.2f}")
        
        self.trades_today = 0
```