Overall Statistics
from AlgorithmImports import *
import math
from calendar import monthrange
from pytz import timezone


class SharpeBasedAthenaMultiSymbol(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetEndDate(2026, 1, 1)
        self.SetCash(100000)

        tickers = ["SPY", "SPLG", "SOXX", "IWB", "VV", "XLI", "IVE", "IWD", "SMH", 'QLD', 'EWT', 'VGT', 'XLK', 'VUG']
        tickerslev = ["TQQQ", "SOXL", "UPRO"]
        stocks = ['NFLX', 'MA', 'NVDA']
        tickers = tickers + tickerslev + stocks

        self.SetWarmUp(timedelta(hours=750))
        self.SetBrokerageModel(BrokerageName.Alpaca, AccountType.Margin)

        self.max_leverage_for_account = 2
        self.leverage = 2
        self.MAX_WEIGHT = 0.3 * self.leverage

        self.symbols = []
        for ticker in tickers:
            security = self.AddEquity(ticker, Resolution.Hour)
            if security:
                self.Debug(f"Added {ticker}")
                security.SetLeverage(self.max_leverage_for_account)
                self.symbols.append(security.Symbol)
            else:
                self.Debug(f"Failed to add {ticker}")

        self.yearly_returns = {s: [] for s in self.symbols}
        self.yearly_prices = {s: None for s in self.symbols}
        self.excluded_symbols = set()

        self.per = 518
        self.mult = 1.0
        self.lookback = 30

        self.price_windows = {s: RollingWindow[float](self.per + 2) for s in self.symbols}
        self.last_filts = {s: None for s in self.symbols}
        self.upward = {s: 0 for s in self.symbols}
        self.downward = {s: 0 for s in self.symbols}
        self.return_windows = {s: RollingWindow[float](self.lookback) for s in self.symbols}
        self.last_prices = {s: None for s in self.symbols}

        self.annual_margin_rate = 0.07
        self.monthly_interest_accrued = 0
        self.last_interest_month = self.Time.month

        self.pending_orders = []
        self.ready_to_trade = False

    def OnWarmupFinished(self):
        self.Debug("Warm-up complete.")
        self.ready_to_trade = True


    def OnData(self, data: Slice):
        if not self.ready_to_trade:
            self.Debug("Still warming up...")
            return

        
        
        self.Debug(f"Is Market Open {self.get_market_status('SPY')}")

        if self.Time.minute % 30 == 0:
            self.Debug(f"Time: {self.Time}, MarginRemaining: {self.Portfolio.MarginRemaining:.2f}")

        if self.pending_orders:
            self.Debug(f"Pending orders: {len(self.pending_orders)} at {self.Time}")
            
            # Iterate over a copy so we can safely modify the original list
            for order in self.pending_orders[:]:
                symbol, quantity, weight, price, side = order

                # Only execute if market is open
                if self.get_market_status(symbol) == True:
                    self.Debug(f"-> Executing {side.upper()} {symbol.Value} qty={quantity}")

                    if side == 'long':
                        self.MarketOrder(symbol, quantity)
                    elif side == 'flat':
                        self.Liquidate(symbol)

                    # Remove this order from the pending list
                    self.pending_orders.remove(order)

        sharpe_scores = {}
        profitable = set()

        for symbol in self.symbols:
            if not data.Bars.ContainsKey(symbol):
                self.Debug(f"No data for {symbol.Value}")
                continue
            if symbol in self.excluded_symbols:
                continue

            bar = data[symbol]
            src = (bar.High + bar.Low) / 2
            self.price_windows[symbol].Add(src)

            last_close = self.last_prices[symbol]
            if last_close is not None and last_close > 0:
                ret = math.log(bar.Close / last_close)
                self.return_windows[symbol].Add(ret)
            self.last_prices[symbol] = bar.Close

            if not self.price_windows[symbol].IsReady or not self.return_windows[symbol].IsReady:
                continue

            window = self.price_windows[symbol]
            price_diffs = [abs(window[i] - window[i - 1]) for i in range(1, self.per + 1)]
            avg_range = sum(price_diffs) / len(price_diffs)
            filt = self.rngfilt(src, avg_range * self.mult, self.last_filts[symbol])
            self.last_filts[symbol] = filt

            if filt is None:
                continue

            if src > filt:
                self.upward[symbol] += 1
                self.downward[symbol] = 0
            elif src < filt:
                self.downward[symbol] += 1
                self.upward[symbol] = 0

            returns = list(self.return_windows[symbol])
            cumulative_ret = sum(returns)
            if cumulative_ret <= 0:
                self.Debug(f"{symbol.Value}: Negative return, skipping")
                continue

            profitable.add(symbol)

            mean_ret = cumulative_ret / len(returns)
            std_dev = math.sqrt(sum((r - mean_ret) ** 2 for r in returns) / len(returns))
            if std_dev > 0:
                sharpe = mean_ret / std_dev
                sharpe_scores[symbol] = sharpe
                self.Debug(f"{symbol.Value} Sharpe: {sharpe:.4f}")

        if not sharpe_scores:
            self.Debug("No sharpe scores, skipping...")
            return

        total_score = sum(max(0, s) for s in sharpe_scores.values())
        if total_score == 0:
            self.Debug("Total sharpe score is 0.")
            return

        raw_allocations = {
            s: max(0, sharpe_scores[s]) / total_score
            for s in sharpe_scores if s in profitable
        }

        allocations = {s: w for s, w in raw_allocations.items() if w >= 1e-4}
        allocations = {s: min(w, self.MAX_WEIGHT) for s, w in allocations.items()}

        portfolio_value = self.Portfolio.TotalPortfolioValue
        available_margin = self.Portfolio.MarginRemaining
        if portfolio_value == 0:
            self.Debug("Portfolio value is 0!")
            return

        max_total_allocation = min(available_margin / portfolio_value, self.leverage)
        total_alloc = sum(allocations.values())
        if total_alloc == 0:
            self.Debug("Final allocations sum to zero, skipping trades.")
            return

        scaling_factor = min(1.0, max_total_allocation / total_alloc)
        allocations = {s: w * scaling_factor for s, w in allocations.items()}

        for symbol in self.symbols:
            if not data.Bars.ContainsKey(symbol) or symbol not in profitable:
                continue

            if not self.price_windows[symbol].IsReady or self.last_filts[symbol] is None:
                continue

            src = self.price_windows[symbol][0]
            filt = self.last_filts[symbol]
            long_condition = src > filt and self.upward[symbol] > 0
            short_condition = src < filt and self.downward[symbol] > 0
            invested = self.Portfolio[symbol].Invested

            self.Debug(f"{symbol.Value}: long={long_condition}, short={short_condition}, invested={invested}")

            if not invested and long_condition and symbol in allocations:
                weight = self.leverage * allocations[symbol]
                target_value = weight * portfolio_value
                price = self.Securities[symbol].Price
                quantity = int(target_value / price)

                if quantity > 0:
                    required_margin = price * quantity / self.Securities[symbol].Leverage
                    if available_margin >= required_margin:
                        if self.get_market_status(symbol) == False:
                            self.pending_orders.append((symbol, quantity, weight, price, 'long'))
                            self.Debug(f"Queued LONG {symbol.Value} qty={quantity}")
                        else:
                            self.Debug(f"MarketOrder LONG {symbol.Value} qty={quantity}")
                            self.MarketOrder(symbol, quantity)

            elif invested and short_condition:
                if self.get_market_status(symbol) == False:
                    quantity = -self.Portfolio[symbol].Quantity
                    price = self.Securities[symbol].Price
                    self.pending_orders.append((symbol, quantity, 0, price, 'flat'))
                    self.Debug(f"Queued SELL {symbol.Value} qty={quantity}")
                else:
                    self.Debug(f"Liquidate {symbol.Value}")
                    self.Liquidate(symbol)

    def rngfilt(self, x, r, prev_filt):
        if prev_filt is None:
            return x
        if x > prev_filt:
            return prev_filt if x - r < prev_filt else x - r
        else:
            return prev_filt if x + r > prev_filt else x + r


    def get_market_status(self, symbol):
        now = self.Time

        # Access the correct Security object from self.Securities
        security = self.Securities[symbol]

        is_open_regular = security.Exchange.Hours.IsOpen(now, extendedMarketHours=False)
        is_open_extended = security.Exchange.Hours.IsOpen(now, extendedMarketHours=True)

        if is_open_regular:
            return True
        elif is_open_extended:
            return False
        else:
            return False

        
    def OnEndOfDay(self):
        holdings_value = self.Portfolio.TotalHoldingsValue
        portfolio_value = self.Portfolio.TotalPortfolioValue
        borrowed = max(0, holdings_value - portfolio_value)
        daily_interest = borrowed * (self.annual_margin_rate / 252)
        self.monthly_interest_accrued += daily_interest

        last_day = monthrange(self.Time.year, self.Time.month)[1]
        if self.Time.day == last_day:
            self.Portfolio.SetCash(self.Portfolio.Cash - self.monthly_interest_accrued)
            self.Debug(f"Monthly interest charged: ${self.monthly_interest_accrued:.2f}")
            self.monthly_interest_accrued = 0

        if self.Time.month == 12 and self.Time.day == 31:
            for symbol in self.symbols:
                if symbol not in self.Securities:
                    continue

                price = self.Securities[symbol].Price
                last_price = self.yearly_prices[symbol]

                if last_price is not None and last_price > 0:
                    yearly_return = (price - last_price) / last_price
                    self.yearly_returns[symbol].append(yearly_return)

                    if len(self.yearly_returns[symbol]) >= 2:
                        if self.yearly_returns[symbol][-1] < 0 and self.yearly_returns[symbol][-2] < 0:
                            self.excluded_symbols.add(symbol)
                            if not self.IsWarmingUp:
                                self.Liquidate(symbol)
                                self.Debug(f"Excluded {symbol.Value} due to 2 bad years")

                self.yearly_prices[symbol] = price
from AlgorithmImports import *

class SymbolData:
    def __init__(self, symbol):
        self.symbol = symbol
        self.bar_data = pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
        self.lower_threshold = 0.2
        self.upper_threshold = 0.8
        self.last_recalibrate_time = datetime.min
        self.consolidator = None

class CustomCryptoFeeModel(FeeModel):
    def __init__(self, fee_rate=0.002):
        self.fee_rate = fee_rate  # 0.2%

    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        order = parameters.Order
        security = parameters.Security

        order_value = order.GetValue(security)
        fee = abs(order_value) * self.fee_rate
        return OrderFee(CashAmount(fee, security.QuoteCurrency.Symbol))


class IBSMultiAssetCrypto15Min(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetCash(100000)
        #self.set_brokerage_model(BrokerageName.KRAKEN, AccountType.CASH)


        self.tickers = ['BTCUSD','SOLUSD','XRPUSD','ETHUSD','SUIUSD']
        #self.tickers = ['BTCUSD']
        self.symbol_data = {}
        self.lookback_bars = 96  # Approx 1 day of 15-min bars
        self.SetWarmUp(timedelta(days=2))  # Enough for 2 days of 15-min bars

        for ticker in self.tickers:
            symbol = self.AddCrypto(ticker, Resolution.Minute, Market.Kraken).Symbol
            sym_data = SymbolData(symbol)
            self.symbol_data[symbol] = sym_data

          
            self.Securities[symbol].FeeModel = CustomCryptoFeeModel(0.00)

            consolidator = TradeBarConsolidator(timedelta(minutes=15))
            consolidator.DataConsolidated += self.OnBar
            self.SubscriptionManager.AddConsolidator(symbol, consolidator)
            sym_data.consolidator = consolidator
    
    
    def CalculateOrderQuantityFromDollar(self, symbol, dollar_amount):
        price = self.Securities[symbol].Price
        if price > 0:
            return dollar_amount / price
        return 0

    

    def get_market_status(self, symbol):
        now = self.Time

        # Access the correct Security object from self.Securities
        security = self.Securities[symbol]

        is_open_regular = security.Exchange.Hours.IsOpen(now, extendedMarketHours=False)
        is_open_extended = security.Exchange.Hours.IsOpen(now, extendedMarketHours=True)

        if is_open_regular:
            return True
        elif is_open_extended:
            return False
        else:
            return False

    def algo_status_debug(self):
        if not self.IsWarmingUp:

            positions = []
            for kvp in self.Portfolio:
                symbol = kvp.Key
                holding = kvp.Value
                if holding.Invested:
                    ticker = symbol.Value
                    avg_price = holding.AveragePrice
                    positions.append(f"{ticker}: ${avg_price:.2f}")

            self.Debug(f"Current Positions: [{', '.join(positions)}]")


    def OnBar(self, sender, bar: TradeBar):

        self.algo_status_debug()
        if self.IsWarmingUp:
            return

        sym_data = self.symbol_data[bar.Symbol]

        new_row = {
            'time': bar.EndTime,
            'open': bar.Open,
            'high': bar.High,
            'low': bar.Low,
            'close': bar.Close,
            'volume': bar.Volume
        }

        sym_data.bar_data = pd.concat([sym_data.bar_data, pd.DataFrame([new_row])], ignore_index=True)
        sym_data.bar_data = sym_data.bar_data.tail(500).reset_index(drop=True)

        if bar.High == bar.Low:
            return

        ibs = (bar.Close - bar.Low) / (bar.High - bar.Low)

 

        invested = self.Portfolio[bar.Symbol].Invested
        target_pct = 1.0 / len(self.symbol_data)

        if ibs < sym_data.lower_threshold and not invested:
            quantity = self.CalculateOrderQuantityFromDollar(bar.Symbol, 20000)
            self.MarketOrder(bar.Symbol, quantity)
        elif ibs >= sym_data.upper_threshold and invested:
            self.Liquidate(bar.Symbol)

        self.Debug(f"{self.Time} | {bar.Symbol.Value} | IBS: {ibs:.2f} | LT: {sym_data.lower_threshold:.2f} | UT: {sym_data.upper_threshold:.2f} | Invested: {invested}")

    def OnEndOfAlgorithm(self):
        total_fees = sum([x.TotalFees for x in self.Portfolio.Values])
        self.Debug(f"Total fees paid: ${total_fees:.2f}")
from AlgorithmImports import *

class SymbolData:
    def __init__(self, symbol):
        self.symbol = symbol
        self.bar_data = pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
        self.lower_threshold = 0.2
        self.upper_threshold = 0.8
        self.last_recalibrate_time = datetime.min
        self.consolidator = None

class CustomCryptoFeeModel(FeeModel):
    def __init__(self, fee_rate=0.002):
        self.fee_rate = fee_rate  # 0.2%

    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        order = parameters.Order
        security = parameters.Security

        order_value = order.GetValue(security)
        fee = abs(order_value) * self.fee_rate
        return OrderFee(CashAmount(fee, security.QuoteCurrency.Symbol))


class IBSMultiAssetCrypto15Min(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetCash(100000)
        #self.set_brokerage_model(BrokerageName.KRAKEN, AccountType.CASH)


        self.tickers = ['BTCUSD','SOLUSD','XRPUSD','ETHUSD','SUIUSD']
        #self.tickers = ['BTCUSD']
        self.symbol_data = {}
        self.lookback_bars = 96  # Approx 1 day of 15-min bars
        self.SetWarmUp(timedelta(days=2))  # Enough for 2 days of 15-min bars

        for ticker in self.tickers:
            symbol = self.AddCrypto(ticker, Resolution.Minute, Market.Kraken).Symbol
            sym_data = SymbolData(symbol)
            self.symbol_data[symbol] = sym_data

          
            self.Securities[symbol].FeeModel = CustomCryptoFeeModel(0.00)

            consolidator = TradeBarConsolidator(timedelta(minutes=15))
            consolidator.DataConsolidated += self.OnBar
            self.SubscriptionManager.AddConsolidator(symbol, consolidator)
            sym_data.consolidator = consolidator
    
    
    def CalculateOrderQuantityFromDollar(self, symbol, dollar_amount):
        price = self.Securities[symbol].Price
        if price > 0:
            return dollar_amount / price
        return 0

    

    def get_market_status(self, symbol):
        now = self.Time

        # Access the correct Security object from self.Securities
        security = self.Securities[symbol]

        is_open_regular = security.Exchange.Hours.IsOpen(now, extendedMarketHours=False)
        is_open_extended = security.Exchange.Hours.IsOpen(now, extendedMarketHours=True)

        if is_open_regular:
            return True
        elif is_open_extended:
            return False
        else:
            return False

    def algo_status_debug(self):
        if not self.IsWarmingUp:

            positions = []
            for kvp in self.Portfolio:
                symbol = kvp.Key
                holding = kvp.Value
                if holding.Invested:
                    ticker = symbol.Value
                    avg_price = holding.AveragePrice
                    positions.append(f"{ticker}: ${avg_price:.2f}")

            self.Debug(f"Current Positions: [{', '.join(positions)}]")


    def OnBar(self, sender, bar: TradeBar):

        self.algo_status_debug()
        if self.IsWarmingUp:
            return

        sym_data = self.symbol_data[bar.Symbol]

        new_row = {
            'time': bar.EndTime,
            'open': bar.Open,
            'high': bar.High,
            'low': bar.Low,
            'close': bar.Close,
            'volume': bar.Volume
        }

        sym_data.bar_data = pd.concat([sym_data.bar_data, pd.DataFrame([new_row])], ignore_index=True)
        sym_data.bar_data = sym_data.bar_data.tail(500).reset_index(drop=True)

        if bar.High == bar.Low:
            return

        ibs = (bar.Close - bar.Low) / (bar.High - bar.Low)

 

        invested = self.Portfolio[bar.Symbol].Invested
        target_pct = 1.0 / len(self.symbol_data)

        if ibs < sym_data.lower_threshold and not invested:
            quantity = self.CalculateOrderQuantityFromDollar(bar.Symbol, 20000)
            self.MarketOrder(bar.Symbol, quantity)
        elif ibs >= sym_data.upper_threshold and invested:
            self.Liquidate(bar.Symbol)

        self.Debug(f"{self.Time} | {bar.Symbol.Value} | IBS: {ibs:.2f} | LT: {sym_data.lower_threshold:.2f} | UT: {sym_data.upper_threshold:.2f} | Invested: {invested}")

    def OnEndOfAlgorithm(self):
        total_fees = sum([x.TotalFees for x in self.Portfolio.Values])
        self.Debug(f"Total fees paid: ${total_fees:.2f}")
from AlgorithmImports import *

class SymbolData:
    def __init__(self, symbol):
        self.symbol = symbol
        self.bar_data = pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
        self.lower_threshold = 0.2
        self.upper_threshold = 0.8
        self.last_recalibrate_time = datetime.min
        self.consolidator = None

class CustomCryptoFeeModel(FeeModel):
    def __init__(self, fee_rate=0.00):
        self.fee_rate = fee_rate  # 0.2%

    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        order = parameters.Order
        security = parameters.Security

        order_value = order.GetValue(security)
        fee = abs(order_value) * self.fee_rate
        return OrderFee(CashAmount(fee, security.QuoteCurrency.Symbol))


class IBSMultiAssetCrypto15Min(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2025, 4, 1)
        self.SetCash(100000)
        self.set_brokerage_model(BrokerageName.KRAKEN, AccountType.CASH)


        self.tickers = ['BTCUSD','SOLUSD','XRPUSD','ETHUSD','SUIUSD']
        #self.tickers = ['BTCUSD']
        self.symbol_data = {}
        self.lookback_bars = 96  # Approx 1 day of 15-min bars
        self.SetWarmUp(timedelta(days=2))  # Enough for 2 days of 15-min bars

        for ticker in self.tickers:
            symbol = self.AddCrypto(ticker, Resolution.Minute, Market.Kraken).Symbol
            sym_data = SymbolData(symbol)
            self.symbol_data[symbol] = sym_data

          
            self.Securities[symbol].FeeModel = CustomCryptoFeeModel(0.00)

            consolidator = TradeBarConsolidator(timedelta(minutes=15))
            consolidator.DataConsolidated += self.OnBar
            self.SubscriptionManager.AddConsolidator(symbol, consolidator)
            sym_data.consolidator = consolidator
    
    
    def CalculateOrderQuantityFromDollar(self, symbol, dollar_amount):
        price = self.Securities[symbol].Price
        if price > 0:
            return dollar_amount / price
        return 0

    

    def get_market_status(self, symbol):
        now = self.Time

        # Access the correct Security object from self.Securities
        security = self.Securities[symbol]

        is_open_regular = security.Exchange.Hours.IsOpen(now, extendedMarketHours=False)
        is_open_extended = security.Exchange.Hours.IsOpen(now, extendedMarketHours=True)

        if is_open_regular:
            return True
        elif is_open_extended:
            return False
        else:
            return False

    def algo_status_debug(self):
        if not self.IsWarmingUp:

            positions = []
            for kvp in self.Portfolio:
                symbol = kvp.Key
                holding = kvp.Value
                if holding.Invested:
                    ticker = symbol.Value
                    avg_price = holding.AveragePrice
                    positions.append(f"{ticker}: ${avg_price:.2f}")

            self.Debug(f"Current Positions: [{', '.join(positions)}]")


    def OnBar(self, sender, bar: TradeBar):

        self.algo_status_debug()
        if self.IsWarmingUp:
            return

        sym_data = self.symbol_data[bar.Symbol]

        new_row = {
            'time': bar.EndTime,
            'open': bar.Open,
            'high': bar.High,
            'low': bar.Low,
            'close': bar.Close,
            'volume': bar.Volume
        }

        sym_data.bar_data = pd.concat([sym_data.bar_data, pd.DataFrame([new_row])], ignore_index=True)
        sym_data.bar_data = sym_data.bar_data.tail(500).reset_index(drop=True)

        if bar.High == bar.Low:
            return

        ibs = (bar.Close - bar.Low) / (bar.High - bar.Low)

 

        invested = self.Portfolio[bar.Symbol].Invested
        target_pct = 1.0 / len(self.symbol_data)

        if ibs < sym_data.lower_threshold and not invested:
            quantity = self.CalculateOrderQuantityFromDollar(bar.Symbol, 20000)
            #self.MarketOrder(bar.Symbol, quantity)

            self.set_holdings(bar.Symbol,0.20)
        elif ibs >= sym_data.upper_threshold and invested:
            self.Liquidate(bar.Symbol)

        self.Debug(f"{self.Time} | {bar.Symbol.Value} | IBS: {ibs:.2f} | LT: {sym_data.lower_threshold:.2f} | UT: {sym_data.upper_threshold:.2f} | Invested: {invested}")

    def OnEndOfAlgorithm(self):
        total_fees = sum([x.TotalFees for x in self.Portfolio.Values])
        self.Debug(f"Total fees paid: ${total_fees:.2f}")