# 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 ```