Overall Statistics
Total Orders
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Start Equity
25000
End Equity
25000
Net Profit
0%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
0
Tracking Error
0
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
Drawdown Recovery
0
#region imports
from AlgorithmImports import *
from datetime import timedelta
from collections import defaultdict
import json
from ticker_config import TICKER_DATA, USE_EXTERNAL_DATA, EXTERNAL_SOURCE_TYPE, EXTERNAL_DATA_URL, PARAMETERS_DATA_URL
from symbol_data import SymbolData, PositionTracker

#https://docs.google.com/spreadsheets/d/e/2PACX-1vS2Nmm1E7qDFr1nZA5hIHABAgDZkjjjsp7d5Bu07j0oxiMz4IwIMVJtkwdJ_SVcByDGmsF9vFhNJ1-D/pub?output=csv
#endregion

class OpeningRangeBreakout(QCAlgorithm):
    """
    Opening Range Breakout Strategy
    
    FEATURES:
    Fetches ticker list from CSV daily
    Loads breakout levels from CSV
    Automatic daily updates before market open
    Fallback to manual ticker list if sheets unavailable
    
    Entry Criteria:
    - 5-min or 30-min opening range breakout
    - Volume surge relative to 20-day average
    - Price < 0.6 * ADR14
    - Price > Breakout Level (from CSV)
    - Distance from 10 EMA (daily) < 0.8 * ADR
    
    Exit Criteria:
    - Hard stop at Low of Day
    - 4R profit: Take 25% off, move stop to 2R
    - Close below 10 EMA within 5 mins of EOD
    """
    
    def Initialize(self):
        # ===== CONFIGURATION =====
        self.SetStartDate(2026, 1, 21)
        self.SetEndDate(2026, 1, 22)
        self.SetCash(25000)
        
        # ===== TICKER LIST CONFIGURATION =====

        self.update_tickers_daily = True  # Reload tickers daily before market open
        self.last_tickers_update = None
        
        # Fallback manual ticker list (if CSV fails)
        self.fallback_tickers = []
        
        # Algorithm control
        self.algorithm_enabled = True  # Master on/off switch
        
        # Risk parameters (default values, will be updated from Google Sheets)
        self.risk_per_trade = 100  # Dollar risk per trade
        self.max_position_pct = 0.15  # 15% max position size
        self.max_positions_per_day = 5
        self.opening_range_minutes = 5  # Can be 5 or 30
        
        # Entry filters
        self.adr_entry_multiplier = 0.6  # Price must be < 0.6 * ADR14
        self.ema_distance_multiplier = 0.8  # Distance from 10 EMA < 0.8 * ADR
        
        # Exit parameters
        self.r_multiple_for_partial = 4  # Take profit at 4R
        self.partial_exit_pct = 0.25  # Take 25% off
        self.trailing_stop_r = 2  # Move stop to 2R after 4R profit
        
        # Track last parameter update
        self.last_parameters_update = None
        
        # ===== DATA STRUCTURES =====
        self.daily_tickers = []  
        self.breakout_levels = {}  
        self.symbols = {}  # Symbol -> SymbolData
        self.positions_opened_today = 0
        self.trading_symbols = []
        self.active_positions = {}  # Symbol -> PositionTracker
        
        # Set resolution and schedule
        self.SetWarmUp(timedelta(days=30))
        self.UniverseSettings.Resolution = Resolution.MINUTE
        
        # Load parameters and tickers from external sources
        self._load_parameters_from_sheets()
        self._load_tickers_from_file()
        
        # Schedule functions
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.At(8, 55),  # 8:55 AM - Load parameters first
            self.UpdateParametersFromSheets
        )
        
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.At(9, 0),  # 9:00 AM - Then load tickers
            self.UpdateTickersFromFile
        )
        
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.AfterMarketOpen("SPY", 0),
            self.OnMarketOpen
        )
        
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.BeforeMarketClose("SPY", 5),
            self.CheckEMAExit
        )
        
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.BeforeMarketClose("SPY", 1),
            self.EndOfDayCleanup
        )
    
    def _load_parameters_from_sheets(self):
        """Load algorithm parameters from Google Sheets"""
        if self.IsWarmingUp:
            return
            
        self.Debug("=" * 60)
        self.Debug("LOADING PARAMETERS FROM GOOGLE SHEETS")
        self.Debug("=" * 60)
        
        try:
            csv_data = self.Download(PARAMETERS_DATA_URL)
            
            if not csv_data:
                self.Debug("Warning: Empty response from parameters source. Using defaults.")
                return
            
            lines = csv_data.strip().split('\n')
            
            # Skip header if present
            start_idx = 1 if lines and ('parameter' in lines[0].lower() or 'name' in lines[0].lower()) else 0
            
            parameters_loaded = 0
            for line in lines[start_idx:]:
                if not line.strip():
                    continue
                
                parts = line.strip().split(',')
                if len(parts) < 2:
                    continue
                
                param_name = parts[0].strip()
                param_value = parts[1].strip()
                
                # Parse and apply parameters
                if param_name.lower() == 'algorithmenabled':
                    self.algorithm_enabled = param_value.upper() in ['TRUE', '1', 'YES', 'ON']
                    self.Debug(f"   Algorithm Enabled: {self.algorithm_enabled}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'riskpertrade':
                    self.risk_per_trade = float(param_value)
                    self.Debug(f"   Risk Per Trade: ${self.risk_per_trade}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'maxpositionpct':
                    self.max_position_pct = float(param_value)
                    self.Debug(f"   Max Position %: {self.max_position_pct * 100}%")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'maxpositionsperday':
                    self.max_positions_per_day = int(float(param_value))
                    self.Debug(f"   Max Positions Per Day: {self.max_positions_per_day}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'openingrangeminutes':
                    self.opening_range_minutes = int(float(param_value))
                    self.Debug(f"   Opening Range Minutes: {self.opening_range_minutes}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'adrentrymultiplier':
                    self.adr_entry_multiplier = float(param_value)
                    self.Debug(f"   ADR Entry Multiplier: {self.adr_entry_multiplier}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'emadistancemultiplier':
                    self.ema_distance_multiplier = float(param_value)
                    self.Debug(f"   EMA Distance Multiplier: {self.ema_distance_multiplier}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'rmultipleforpartial':
                    self.r_multiple_for_partial = float(param_value)
                    self.Debug(f"   R Multiple For Partial: {self.r_multiple_for_partial}")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'partialexitpct':
                    self.partial_exit_pct = float(param_value)
                    self.Debug(f"   Partial Exit %: {self.partial_exit_pct * 100}%")
                    parameters_loaded += 1
                    
                elif param_name.lower() == 'trailingstopr':
                    self.trailing_stop_r = float(param_value)
                    self.Debug(f"   Trailing Stop R: {self.trailing_stop_r}")
                    parameters_loaded += 1
            
            self.last_parameters_update = self.Time
            self.Debug(f"SUCCESS: Loaded {parameters_loaded} parameters")
            
            if not self.algorithm_enabled:
                self.Debug("*** ALGORITHM IS DISABLED - NO TRADES WILL BE PLACED ***")
            
            self.Debug("=" * 60)
            
        except Exception as e:
            self.Debug(f"ERROR loading parameters: {str(e)}")
            self.Debug("Using default parameter values")
            self.Debug("=" * 60)
    
    def UpdateParametersFromSheets(self):
        """Reload parameters from Google Sheets daily"""
        if self.last_parameters_update and self.last_parameters_update.date() == self.Time.date():
            self.Debug("Parameters already updated today. Skipping.")
            return
        
        self._load_parameters_from_sheets()
    
    def _load_tickers_from_file(self):
        """Load tickers from ticker_config.py or external source"""
        if self.IsWarmingUp:
            return
            
        self.Debug("=" * 60)
        self.Debug("LOADING TICKERS")
        self.Debug("=" * 60)
        
        try:
            self.daily_tickers = []
            self.breakout_levels = {}
            
            # Check if using external data source
            if USE_EXTERNAL_DATA:
                self.Debug(f"Loading from external source: {EXTERNAL_SOURCE_TYPE}")
                self._load_from_external_source()
            else:
                self.Debug("Loading from ticker_config.py TICKER_DATA")
                # Load from TICKER_DATA
                for item in TICKER_DATA:
                    ticker = item[0].upper()
                    self.daily_tickers.append(ticker)
                    
                    # Get breakout level if available
                    if len(item) >= 2:
                        try:
                            self.breakout_levels[ticker] = float(item[1])
                        except (ValueError, TypeError):
                            self.Debug(f"Warning: Invalid breakout level for {ticker}")
            
            self.last_tickers_update = self.Time
            
            self.Debug(f"SUCCESS: Loaded {len(self.daily_tickers)} tickers")
            self.Debug(f"Tickers: {', '.join(self.daily_tickers)}")
            if self.breakout_levels:
                self.Debug(f"Breakout levels loaded for {len(self.breakout_levels)} tickers")
                for ticker, level in self.breakout_levels.items():
                    self.Debug(f"   {ticker}: ${level:.2f}")
            self.Debug("=" * 60)
            
            # Initialize symbols
            self._update_symbols()
            
        except Exception as e:
            self.Debug(f"ERROR: Failed to load tickers: {str(e)}")
            self.daily_tickers = self.fallback_tickers
            self._update_symbols()
    
    def _load_from_external_source(self):
        """Fetch and parse tickers from external data source"""
        try:
            csv_data = self.Download(EXTERNAL_DATA_URL)
            
            if not csv_data:
                raise Exception("Empty response from external source")
            
            lines = csv_data.strip().split('\n')
            
            # Skip header if present
            start_idx = 1 if lines and ('ticker' in lines[0].lower() or 'symbol' in lines[0].lower()) else 0
            
            for line in lines[start_idx:]:
                if not line.strip():
                    continue
                
                parts = line.strip().split(',')
                if not parts:
                    continue
                
                ticker = parts[0].strip().upper()
                if not ticker:
                    continue
                
                self.daily_tickers.append(ticker)
                
                # Parse breakout level if available
                if len(parts) >= 2 and parts[1].strip():
                    try:
                        breakout_level = float(parts[1].strip())
                        self.breakout_levels[ticker] = breakout_level
                    except (ValueError, TypeError):
                        self.Debug(f"Warning: Invalid breakout level for {ticker}")
            
            if not self.daily_tickers:
                raise Exception("No tickers found in external source")
            
            self.Debug(f"Successfully loaded {len(self.daily_tickers)} tickers from external source")
            
        except Exception as e:
            self.Debug(f"ERROR loading from external source: {str(e)}")
            self.Debug("Falling back to TICKER_DATA from ticker_config.py")
            # Fallback to internal TICKER_DATA
            for item in TICKER_DATA:
                ticker = item[0].upper()
                self.daily_tickers.append(ticker)
                if len(item) >= 2:
                    try:
                        self.breakout_levels[ticker] = float(item[1])
                    except (ValueError, TypeError):
                        pass
    
    def UpdateTickersFromFile(self):
        """Reload ticker list from Google Sheets"""
        
        # Skip if already updated today
        if self.last_tickers_update and self.last_tickers_update.date() == self.Time.date():
            self.Debug("Tickers already updated today. Skipping.")
            return
        
        self._load_tickers_from_file()
    
    def _update_symbols(self):
        """Initialize or update symbols based on current ticker list"""
        # Add new symbols
        for ticker in self.daily_tickers:
            # Check if symbol already exists
            symbol_exists = any(str(sym).split()[0] == ticker for sym in self.symbols.keys())
            
            if not symbol_exists:
                try:
                    symbol = self.AddEquity(ticker, Resolution.Minute).Symbol
                    
                    # Create symbol data container
                    symbol_data = SymbolData(symbol, self)
                    
                    # Set breakout level if available
                    if ticker in self.breakout_levels:
                        symbol_data.breakout_level = self.breakout_levels[ticker]
                        self.Debug(f"   Set breakout level for {ticker}: ${symbol_data.breakout_level:.2f}")
                    
                    self.symbols[symbol] = symbol_data
                    
                    # Setup consolidators
                    self._setup_consolidators(symbol, symbol_data)
                    
                    # Pre-populate indicators with historical data
                    self._seed_indicators(symbol, symbol_data)
                    
                    self.Debug(f"Added symbol: {ticker}")
                    
                except Exception as e:
                    self.Debug(f"Failed to add {ticker}: {str(e)}")
    
    def _setup_consolidators(self, symbol, symbol_data):
        """Setup minute and daily consolidators for indicators"""
        # Daily consolidator for EMA and ADR
        daily_consolidator = TradeBarConsolidator(timedelta(days=1))
        daily_consolidator.DataConsolidated += symbol_data.OnDailyBar
        self.SubscriptionManager.AddConsolidator(symbol, daily_consolidator)
        
        # 5-minute consolidator for opening range
        five_min_consolidator = TradeBarConsolidator(timedelta(minutes=5))
        five_min_consolidator.DataConsolidated += symbol_data.OnFiveMinuteBar
        self.SubscriptionManager.AddConsolidator(symbol, five_min_consolidator)
    
    def _seed_indicators(self, symbol, symbol_data):
        """Pre-populate indicators using historical data"""
        try:
            # Get historical daily data (30 days for EMA and ADR)
            history = self.History(symbol, 30, Resolution.Daily)
            
            if history.empty:
                self.Debug(f"No historical data available for {symbol}")
                return
            
            self.Debug(f"Seeding indicators for {symbol} with {len(history)} days of data")
            
            # Feed historical data to indicators
            for idx, row in history.iterrows():
                # Extract time from the MultiIndex (level 1 is the time)
                time = idx[1] if isinstance(idx, tuple) else idx
                
                # Update EMA
                symbol_data.ema_10.Update(time, row['close'])
                
                # Update volume average
                symbol_data.avg_volume_20.Update(time, row['volume'])
                
                # Update ADR
                daily_range = row['high'] - row['low']
                symbol_data.daily_ranges.Add(daily_range)
            
            self.Debug(f"{symbol}: EMA ready={symbol_data.ema_10.IsReady}, Vol ready={symbol_data.avg_volume_20.IsReady}, ADR ready={symbol_data.daily_ranges.IsReady}")
            
        except Exception as e:
            self.Debug(f"Error seeding indicators for {symbol}: {str(e)}")
    
    def OnMarketOpen(self):
        """Reset daily tracking at market open"""
        self.positions_opened_today = 0
        self.trading_symbols = []
        
        # Reset symbol data for the new day
        for symbol_data in self.symbols.values():
            symbol_data.Reset()
        
        # Debug: Check which symbols are ready
        ready_count = sum(1 for sd in self.symbols.values() if sd.is_ready())
        self.Debug(f"Market opened. Ready to trade {len(self.daily_tickers)} symbols")
        self.Debug(f"Symbols loaded: {', '.join(self.daily_tickers[:10])}" +
                  (f" +{len(self.daily_tickers)-10} more" if len(self.daily_tickers) > 10 else ""))
        self.Debug(f"Symbols ready for trading: {ready_count}/{len(self.symbols)}")
        
        for sym, sym_data in list(self.symbols.items())[:5]:
            self.Debug(f"  {sym}: EMA={sym_data.ema_10.IsReady}, Vol={sym_data.avg_volume_20.IsReady}, ADR={sym_data.daily_ranges.IsReady}, OR={sym_data.opening_range_high is not None}")
    
    def OnData(self, data):
        """Main data handler - check for entries and manage positions"""
        if self.IsWarmingUp:
            return
        
        # Check if algorithm is enabled
        if not self.algorithm_enabled:
            return
        
        if self.positions_opened_today < self.max_positions_per_day:
            self._check_entry_signals(data)
        
        # Manage existing positions
        self._manage_positions(data)
    
    def _check_entry_signals(self, data):
        """Check for opening range breakout with all filters"""
        current_time = self.Time
        
        # Only trade during opening range period
        market_open = self.Time.replace(hour=9, minute=30, second=0, microsecond=0)
        minutes_from_open = (current_time - market_open).total_seconds() / 60
        
        if minutes_from_open < 0 or minutes_from_open > self.opening_range_minutes + 30:
            return
        
        for symbol, symbol_data in self.symbols.items():
            # Skip if already in position
            if symbol in self.active_positions:
                continue
            
            # Skip if not enough data
            if not symbol_data.is_ready():
                self.Debug(f"{symbol}: Not ready - EMA: {symbol_data.ema_10.IsReady}, Vol: {symbol_data.avg_volume_20.IsReady}, ADR: {symbol_data.daily_ranges.IsReady}, OR: {symbol_data.opening_range_high is not None}")
                continue
            
            # Skip if no current price
            if symbol not in data.Bars:
                self.Debug(f"{symbol}: No bar data available")
                continue
            
            bar = data.Bars[symbol]
            
            # Check all entry conditions
            if self._check_entry_conditions(symbol, symbol_data, bar, minutes_from_open):
                self._enter_position(symbol, symbol_data, bar)
            else:
                self.Debug(f"{symbol}: Entry conditions not met at {current_time}")
    
    def _check_entry_conditions(self, symbol, symbol_data, bar, minutes_from_open):
        """Check all entry conditions for a potential trade"""
        
        # 1. Check if opening range is established
        if not symbol_data.opening_range_high:
            self.Debug(f"{symbol}: Opening range not established")
            return False
        
        # 2. Check opening range breakout
        if bar.Close <= symbol_data.opening_range_high:
            self.Debug(f"{symbol}: No breakout - Close {bar.Close:.2f} <= OR High {symbol_data.opening_range_high:.2f}")
            return False
        
        # 3. Calculate current ADR
        adr = symbol_data.get_adr()
        if adr is None or adr == 0:
            self.Debug(f"{symbol}: ADR invalid - {adr}")
            return False
        
        # 4. Price must be < 0.6 * ADR14
        adr_threshold = self.adr_entry_multiplier * adr
        if bar.Close >= adr_threshold:
            self.Debug(f"{symbol}: Price too high - {bar.Close:.2f} >= {adr_threshold:.2f} (0.6*ADR)")
            return False
        
        # 5. Check volume surge
        volume_ratio = self._calculate_volume_surge(symbol_data, minutes_from_open)
        required_surge = self._get_required_volume_surge(minutes_from_open)
        
        if volume_ratio < required_surge:
            self.Debug(f"{symbol}: Vol surge too low - {volume_ratio:.2%} < {required_surge:.2%}")
            return False
        
        # 6. Check distance from 10 EMA (daily)
        ema_10 = symbol_data.ema_10.Current.Value
        distance_from_ema = abs(bar.Close - ema_10)
        ema_threshold = self.ema_distance_multiplier * adr
        
        if distance_from_ema >= ema_threshold:
            self.Debug(f"{symbol}: EMA distance too large - {distance_from_ema:.2f} >= {ema_threshold:.2f}")
            return False
        
        # 7. Check breakout level if available
        if symbol_data.breakout_level and bar.Close <= symbol_data.breakout_level:
            self.Debug(f"{symbol}: Price {bar.Close:.2f} <= Breakout Level {symbol_data.breakout_level:.2f}")
            return False
        
        self.Debug(f"ALL ENTRY CONDITIONS MET for {symbol}")
        self.Debug(f"   Close: ${bar.Close:.2f}, OR High: ${symbol_data.opening_range_high:.2f}, ADR: ${adr:.2f}")
        self.Debug(f"   Volume surge: {volume_ratio:.2%}, EMA dist: {distance_from_ema:.2f}")
        if symbol_data.breakout_level:
            self.Debug(f"   Breakout level: ${symbol_data.breakout_level:.2f}")
        return True
    
    def _calculate_volume_surge(self, symbol_data, minutes_from_open):
        """Calculate volume surge relative to 20-day average"""
        if symbol_data.avg_volume_20.Current.Value == 0:
            return 0
        
        if minutes_from_open <= 0:
            return 0
        
        volume_per_minute = symbol_data.current_day_volume / minutes_from_open
        avg_volume_per_minute = symbol_data.avg_volume_20.Current.Value / 390
        
        return volume_per_minute / avg_volume_per_minute if avg_volume_per_minute > 0 else 0
    
    def _get_required_volume_surge(self, minutes_from_open):
        """Get required volume surge based on time from open"""
        if minutes_from_open <= 5:
            return 0.10
        elif minutes_from_open <= 10:
            return 0.20
        elif minutes_from_open <= 30:
            return 0.50
        else:
            return 1.0
    
    def _enter_position(self, symbol, symbol_data, bar):
        """Enter a new position with proper position sizing and risk management"""
        
        # Calculate position size
        stop_loss = symbol_data.low_of_day
        entry_price = bar.Close
        risk_per_share = entry_price - stop_loss
        
        if risk_per_share <= 0:
            return
        
        # Calculate shares based on dollar risk
        shares_by_risk = int(self.risk_per_trade / risk_per_share)
        
        # Calculate shares based on max position size
        max_position_value = self.Portfolio.TotalPortfolioValue * self.max_position_pct
        shares_by_max_position = int(max_position_value / entry_price)
        
        # Take the minimum
        shares = min(shares_by_risk, shares_by_max_position)
        
        if shares <= 0:
            return
        
        # Execute the trade
        ticket = self.MarketOrder(symbol, shares)
        
        if ticket.Status == OrderStatus.Filled:
            # Create position tracker
            position_tracker = PositionTracker(
                symbol=symbol,
                entry_price=entry_price,
                shares=shares,
                stop_loss=stop_loss,
                r_value=risk_per_share
            )
            
            self.active_positions[symbol] = position_tracker
            self.positions_opened_today += 1
            
            # Place stop loss order
            self._place_stop_loss(symbol, shares, stop_loss)
            
            self.Debug("=" * 60)
            self.Debug(f"ENTRY: {symbol}")
            self.Debug(f"   Shares: {shares}, Entry: ${entry_price:.2f}")
            self.Debug(f"   Stop: ${stop_loss:.2f}, Risk/Share: ${risk_per_share:.2f}")
            if symbol_data.breakout_level:
                self.Debug(f"   Breakout Level: ${symbol_data.breakout_level:.2f}")
            self.Debug("=" * 60)
    
    def _place_stop_loss(self, symbol, shares, stop_price):
        """Place a Good-Till-Cancel stop loss order"""
        stop_ticket = self.StopMarketOrder(symbol, -shares, stop_price)
        stop_ticket.OrderProperties.TimeInForce = TimeInForce.GoodTilCanceled
        
        if symbol in self.active_positions:
            self.active_positions[symbol].stop_ticket = stop_ticket
    
    def _manage_positions(self, data):
        """Manage existing positions - check for profit targets and trailing stops"""
        for symbol, position in list(self.active_positions.items()):
            if symbol not in data.Bars:
                continue
            
            current_price = data.Bars[symbol].Close
            profit = current_price - position.entry_price
            r_multiple = profit / position.r_value if position.r_value > 0 else 0
            
            # Check for 4R profit target
            if not position.partial_exit_done and r_multiple >= self.r_multiple_for_partial:
                self._take_partial_profit(symbol, position)
                self._move_stop_to_trailing(symbol, position)
    
    def _take_partial_profit(self, symbol, position):
        """Take 25% profit at 4R"""
        shares_to_sell = int(position.shares * self.partial_exit_pct)
        
        if shares_to_sell > 0:
            self.MarketOrder(symbol, -shares_to_sell)
            position.shares -= shares_to_sell
            position.partial_exit_done = True
            
            self.Debug(f"PARTIAL EXIT: {symbol} - Sold {shares_to_sell} shares at 4R")
    
    def _move_stop_to_trailing(self, symbol, position):
        """Move stop loss to 2R after taking partial profit"""
        new_stop = position.entry_price + (self.trailing_stop_r * position.r_value)
        
        if position.stop_ticket:
            position.stop_ticket.Cancel()
        
        self._place_stop_loss(symbol, position.shares, new_stop)
        position.stop_loss = new_stop
        
        self.Debug(f"TRAILING STOP: {symbol} - New stop at ${new_stop:.2f} (2R)")
    
    def CheckEMAExit(self):
        """Check for EMA exit condition within 5 mins of close"""
        symbols_to_exit = []
        
        for symbol, position in self.active_positions.items():
            symbol_data = self.symbols.get(symbol)
            if not symbol_data:
                continue
            
            if not self.Securities[symbol].Price:
                continue
            
            current_price = self.Securities[symbol].Price
            ema_10 = symbol_data.ema_10.Current.Value
            
            if current_price < ema_10:
                symbols_to_exit.append(symbol)
        
        for symbol in symbols_to_exit:
            self._exit_position(symbol, "EMA Exit")
    
    def _exit_position(self, symbol, reason):
        """Exit a position completely"""
        if symbol not in self.active_positions:
            return
        
        position = self.active_positions[symbol]
        
        if position.stop_ticket:
            position.stop_ticket.Cancel()
        
        self.Liquidate(symbol)
        
        del self.active_positions[symbol]
        
        self.Debug(f"EXIT: {symbol} - Reason: {reason}")
    
    def EndOfDayCleanup(self):
        """Close all positions at end of day"""
        for symbol in list(self.active_positions.keys()):
            self._exit_position(symbol, "End of Day")
    
    def OnOrderEvent(self, orderEvent):
        """Handle order events"""
        if orderEvent.Status == OrderStatus.Filled:
            self.Debug(f"Order filled: {orderEvent.Symbol} {orderEvent.Direction} " +
                      f"{orderEvent.FillQuantity} @ ${orderEvent.FillPrice:.2f}")
from AlgorithmImports import *
from datetime import timedelta


class SymbolData:
    """Container for symbol-specific data and indicators"""
    
    def __init__(self, symbol, algorithm):
        self.symbol = symbol
        self.algorithm = algorithm
        
        # Indicators
        self.ema_10 = ExponentialMovingAverage(10)
        self.avg_volume_20 = SimpleMovingAverage(20)
        self.daily_ranges = RollingWindow[float](14)
        
        # Opening range tracking
        self.opening_range_high = None
        self.opening_range_low = None
        self.low_of_day = None
        self.high_of_day = None
        
        # Volume tracking
        self.current_day_volume = 0
        self.daily_volume_history = RollingWindow[float](20)
        
        # Breakout level
        self.breakout_level = None
        
        self.last_daily_bar = None
    
    def OnDailyBar(self, sender, bar):
        """Process daily bars for EMA and ADR"""
        self.ema_10.Update(bar.EndTime, bar.Close)
        
        daily_range = bar.High - bar.Low
        self.daily_ranges.Add(daily_range)
        
        self.daily_volume_history.Add(bar.Volume)
        self.avg_volume_20.Update(bar.EndTime, bar.Volume)
        
        self.last_daily_bar = bar
        
        # Debug
        if self.daily_ranges.IsReady:
            adr = sum(self.daily_ranges) / self.daily_ranges.Count
            self.algorithm.Debug(f"{self.symbol} daily bar: Close={bar.Close:.2f}, ADR={adr:.2f}, EMA_Ready={self.ema_10.IsReady}")
    
    def OnFiveMinuteBar(self, sender, bar):
        """Process 5-minute bars for opening range"""
        market_open = bar.EndTime.replace(hour=9, minute=30, second=0, microsecond=0)
        
        if bar.EndTime <= market_open + timedelta(minutes=5):
            if self.opening_range_high is None:
                self.opening_range_high = bar.High
                self.opening_range_low = bar.Low
                self.low_of_day = bar.Low
                self.high_of_day = bar.High
                self.algorithm.Debug(f"{self.symbol} 5min bar (opening range): High={bar.High:.2f}, Low={bar.Low:.2f}, Time={bar.EndTime}")
            else:
                self.opening_range_high = max(self.opening_range_high, bar.High)
                self.opening_range_low = min(self.opening_range_low, bar.Low)
                self.algorithm.Debug(f"{self.symbol} 5min bar (in range): OR High={self.opening_range_high:.2f}, OR Low={self.opening_range_low:.2f}")
        
        if self.low_of_day is None:
            self.low_of_day = bar.Low
        else:
            self.low_of_day = min(self.low_of_day, bar.Low)
        
        if self.high_of_day is None:
            self.high_of_day = bar.High
        else:
            self.high_of_day = max(self.high_of_day, bar.High)
        
        self.current_day_volume += bar.Volume
    
    def get_adr(self):
        """Calculate Average Daily Range over 14 days"""
        if not self.daily_ranges.IsReady:
            return None
        return sum(self.daily_ranges) / self.daily_ranges.Count
    
    def is_ready(self):
        """Check if all indicators are ready"""
        return (self.ema_10.IsReady and 
                self.avg_volume_20.IsReady and 
                self.daily_ranges.IsReady and
                self.opening_range_high is not None)
    
    def Reset(self):
        """Reset intraday values at start of new day"""
        self.opening_range_high = None
        self.opening_range_low = None
        self.low_of_day = None
        self.high_of_day = None
        self.current_day_volume = 0


class PositionTracker:
    """Track individual position details"""
    
    def __init__(self, symbol, entry_price, shares, stop_loss, r_value):
        self.symbol = symbol
        self.entry_price = entry_price
        self.shares = shares
        self.stop_loss = stop_loss
        self.r_value = r_value
        self.partial_exit_done = False
        self.stop_ticket = None
# region imports
from AlgorithmImports import *
# endregion

# Ticker Configuration
# Format: (Ticker, BreakoutLevel, Notes)

# Internal ticker list (used when USE_EXTERNAL_DATA = False or as fallback)
TICKER_DATA = [
    ("AMD", 242.85, "Apple"),
    ("TSLA", 424.15, "Tesla"),
]


# ===== EXTERNAL DATA SOURCE CONFIGURATION =====
# Set USE_EXTERNAL_DATA = True to pull from external source instead of TICKER_DATA above

USE_EXTERNAL_DATA = True

# Choose your external data source:
# Option 1: CSV file hosted online
EXTERNAL_SOURCE_TYPE = "csv_url"  # Options: "csv_url", "json_url", "pastebin"
EXTERNAL_DATA_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vS2Nmm1E7qDFr1nZA5hIHABAgDZkjjjsp7d5Bu07j0oxiMz4IwIMVJtkwdJ_SVcByDGmsF9vFhNJ1-D/pub?output=csv"

# Google Sheets Parameters URL (separate sheet or range for parameters)
# Expected CSV format:
# ParameterName,Value
# AlgorithmEnabled,TRUE
# RiskPerTrade,100
# MaxPositionPct,0.15
# MaxPositionsPerDay,5
# OpeningRangeMinutes,5
# ADREntryMultiplier,0.6
# EMADistanceMultiplier,0.8
# RMultipleForPartial,4
# PartialExitPct,0.25
# TrailingStopR,2
PARAMETERS_DATA_URL = "https://docs.google.com/spreadsheets/d/e/2PACX-1vS2Nmm1E7qDFr1nZA5hIHABAgDZkjjjsp7d5Bu07j0oxiMz4IwIMVJtkwdJ_SVcByDGmsF9vFhNJ1-D/pub?gid=454864536&single=true&output=csv"

# Option 2: JSON file (expected format below)
# EXTERNAL_SOURCE_TYPE = "json_url"
# EXTERNAL_DATA_URL = "https://example.com/tickers.json"
# Expected JSON format:
# {
#     "tickers": ["AAPL", "TSLA", "AMD"],
#     "breakout_levels": {"AAPL": 240.00, "TSLA": 380.00}
# }

# CSV URL Format for tickers:
# Ticker,BreakoutLevel,Notes
# AAPL,240.00,Apple
# TSLA,380.00,Tesla