| 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