Overall Statistics
#region imports
from AlgorithmImports import *
#endregion


# Your New Python File
class MomentumAlpha(AlphaModel):
    
    def __init__(
        self, 
        long_period=252, #12-month
        short_period=21, #1-month
        long_percent=0.2, #quintile
        short_percent=0.2
    ):
        self.long_period = long_period
        self.short_period = short_period
        self.long_percent = long_percent
        self.short_percent = short_percent
        self.mom_by_symbol = {}
        self.last_month = -1
    

    def update(self, algorithm, data):
        # emit monthly insight for now
        if self.last_month == algorithm.Time.month:
            return []
        self.last_month = algorithm.Time.month

        mom_scores = []
        for symbol in self.mom_by_symbol:
            if self.mom_by_symbol[symbol].is_ready():
                #algorithm.log(f"{algorithm.Time}: {symbol} is ready")
                long_momentum = self.mom_by_symbol[symbol].get_momentum_percent(self.short_period, self.long_period-1)
                short_momentum = self.mom_by_symbol[symbol].get_momentum_percent(0, self.short_period-1)
                mom_scores.append([symbol, long_momentum, short_momentum])
        # size_in_bytes = sys.getsizeof(self.mom_by_symbol[symbol])
        # algorithm.log(f"mom rolling window size: {size_in_bytes}")
                
        if not mom_scores:
            return []
        
        def assign_quintiles(data):
            return pd.qcut(data, 5, labels=[1, 2, 3, 4, 5])

        # algorithm.log(f"{algorithm.Time}: Number of assets available is {len(mom_scores)}")
        mom_scores_df = pd.DataFrame(mom_scores, columns=['symbol', 'long_momentum', 'short_momentum'])
        mom_scores_df['long_momentum_quintile'] = assign_quintiles(mom_scores_df['long_momentum'])
        mom_scores_df['short_momentum_quintile'] = assign_quintiles(mom_scores_df['short_momentum'])

        long_stocks = mom_scores_df[(mom_scores_df['long_momentum_quintile'] == 5) & (mom_scores_df['short_momentum_quintile'] == 1)]
        short_stocks = mom_scores_df[(mom_scores_df['long_momentum_quintile'] == 1) & (mom_scores_df['short_momentum_quintile'] == 5)]

        algorithm.log(f"Long Stocks: {long_stocks['symbol']}")
        algorithm.log(f"Short Stocks: {short_stocks['symbol']}")
        
        insights = []
        for symbol in long_stocks['symbol'].unique():
            insights.append(Insight.Price(symbol, timedelta(days=20), InsightDirection.Up, None, None))
        for symbol in short_stocks['symbol'].unique():
            insights.append(Insight.Price(symbol, timedelta(days=20), InsightDirection.Down, None, None))

        return insights
    


    def on_securities_changed(self, algorithm, changes):
        for security in changes.RemovedSecurities:
            # algorithm.log(f"{algorithm.time}: Removed {security.Symbol}")
            if security.Symbol in self.mom_by_symbol:
                del self.mom_by_symbol[security.Symbol]
                
        for security in changes.AddedSecurities:
            # algorithm.log(f"{algorithm.time}: Added {security.Symbol}")
            if security not in self.mom_by_symbol:
                history_by_symbol = algorithm.History(security.Symbol, self.long_period, Resolution.DAILY)
                self.mom_by_symbol[security.Symbol] = LongMomentumShortReversalIndicator(security.Symbol, self.long_period, algorithm, history_by_symbol)
                #register indicator for daily update
                algorithm.RegisterIndicator(security.Symbol, self.mom_by_symbol[security.Symbol], Resolution.DAILY)
                del history_by_symbol
                
class LongMomentumShortReversalIndicator(PythonIndicator):
    
    def __init__(self, symbol, period, algorithm, history):
        self.symbol = symbol
        self.period = period
        self.value = 0
        self.rollingWindow = RollingWindow[float](self.period)
        #warm up indicator with history
        if not history.empty:
            for _, tradebar in history.loc[symbol].iterrows():
                self.update(tradebar)
            # algorithm.log(f"{algorithm.Time}: {symbol}")
        
    
    def update(self, bar):
        self.rollingWindow.add(bar.close)
        return self.rollingWindow.is_ready
    
    def is_ready(self):
        return self.rollingWindow.is_ready
    
    # percentage momentum between time_0 and time_n calculated as:
    # (price[time_0] - price[time_n]) / price[time_n]
    def get_momentum_percent(self, time_0, time_n):
        return 100 * (self.rollingWindow[time_0] - self.rollingWindow[time_n]) / self.rollingWindow[time_n]
from AlgorithmImports import *

class TopCryptoStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2024, 3, 1)  # Set Start Date
        self.SetEndDate(2024, 6, 1)
        self.SetCash(100000)  # Set Strategy Cash

        # Define the symbols
        self.crypto_symbols = ["BTCUSD"]
        self.SetBenchmark("SPY")

        # Attempt to add each cryptocurrency and stock
        self.active_symbols = []
        for symbol in self.crypto_symbols:
            try:
                self.AddCrypto(symbol, Resolution.MINUTE)
                self.active_symbols.append(symbol)
            except Exception as e:
                self.Debug(f"Unable to add symbol: {symbol}. Exception: {e}")

        

        # Define the technical indicators
        self.supertrend1 = {}
        self.supertrend2 = {}
        self.rsi = {}
        self.ema100 = {}
        self.weekly_twap = {}
        self.entry_prices = {}

        for symbol in self.active_symbols:
            self.supertrend1[symbol] = self.STR(symbol, 10, 10, MovingAverageType.Wilders)
            self.supertrend2[symbol] = self.STR(symbol, 10, 3, MovingAverageType.Wilders)
            self.rsi[symbol] = self.RSI(symbol, 10, MovingAverageType.Wilders, Resolution.MINUTE)
            self.ema100[symbol] = self.EMA(symbol, 100, Resolution.MINUTE)
            self.weekly_twap[symbol] = self.WeeklyTwap(symbol, 5)
            self.entry_prices[symbol] = None

        self.SetWarmUp(100, Resolution.MINUTE)  # Warm up period for 100 days

    def WeeklyTwap(self, symbol, num_weeks):
        twap = self.SMA(symbol, num_weeks * 5, Resolution.MINUTE)  # Assuming 5 trading days per week
        return twap

    def OnData(self, data):
        if self.IsWarmingUp:
            return

        for symbol in self.active_symbols:
            if not data.Bars.ContainsKey(symbol):
                continue

            bar = data.Bars[symbol]

            # Get current values
            current_price = bar.Close
            supertrend1 = self.supertrend1[symbol].Current.Value
            supertrend2 = self.supertrend2[symbol].Current.Value
            rsi = self.rsi[symbol].Current.Value
            ema100 = self.ema100[symbol].Current.Value
            weekly_twap = self.weekly_twap[symbol].Current.Value

            # Define factor based on asset type
            factor = 1.2

            # Entry condition
            if self.entry_prices[symbol] is None:
                if ( rsi < 99):  # Use appropriate factor
                    self.Debug(f"{symbol}: Supertrend1={supertrend1}, Supertrend2={supertrend2}, RSI={rsi}, EMA100={ema100}, Weekly TWAP={weekly_twap}")
                    self.SetHoldings(symbol, -1.0)
                    self.entry_prices[symbol] = current_price
            
            # Exit condition
            elif rsi < 1:
                self.Liquidate(symbol)
                self.entry_prices[symbol] = None