Overall Statistics
Total Orders
35
Average Win
1.10%
Average Loss
-0.01%
Compounding Annual Return
205.059%
Drawdown
18.200%
Expectancy
71.010
Start Equity
4000
End Equity
7996.87
Net Profit
99.922%
Sharpe Ratio
3.785
Sortino Ratio
4.461
Probabilistic Sharpe Ratio
92.647%
Loss Rate
14%
Win Rate
86%
Profit-Loss Ratio
83.01
Alpha
0
Beta
0
Annual Standard Deviation
0.324
Annual Variance
0.105
Information Ratio
3.955
Tracking Error
0.324
Treynor Ratio
0
Total Fees
$35.00
Estimated Strategy Capacity
$6200000.00
Lowest Capacity Asset
GLD T3SKPOF94JFP
Portfolio Turnover
2.94%
Drawdown Recovery
62
# region imports
from AlgorithmImports import *
import numpy as np
import math
# endregion

class CfdSimulationBuyingPowerModel(SecurityMarginModel):
    def __init__(self, initial_margin):
        # Initial Margin (z.B. 0.1313) -> Leverage = 1 / 0.1313 = ca. 7.6
        super().__init__(1.0 / initial_margin)

class Kelly9SigTQQQ_UUP_MarginOptimized(QCAlgorithm):

    def initialize(self):
        self.set_start_date(2025, 6, 1)
        self.set_cash(4000)
        self._is_live = self.live_mode
        
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)

        # --- IBKR ZINS-PARAMETER ---
        self.bm_rate = 0.0364               # Benchmark 3.64%
        self.borrow_rate = 0.0514           # BM + 1.5% = 5.14% (Sollzins/Financing)
        self.credit_rate = 0.0314           # BM - 0.5% = 3.14% (Habenzins)
        
        # 1. MARGIN-PARAMETER
        self.qqq_margin_req = 0.1313  
        self.uup_margin_req = 0.20    

        if self._is_live:
            self.qqq = self.add_cfd("QQQ", Resolution.MINUTE, Market.INTERACTIVE_BROKERS).symbol
            self.safe = self.add_cfd("GLD", Resolution.MINUTE, Market.INTERACTIVE_BROKERS).symbol
        else:
            self.qqq_obj = self.add_equity("QQQ", Resolution.MINUTE)
            self.safe_obj = self.add_equity("GLD", Resolution.MINUTE)
            self.qqq = self.qqq_obj.symbol
            self.safe = self.safe_obj.symbol
            
            self.qqq_obj.set_buying_power_model(CfdSimulationBuyingPowerModel(self.qqq_margin_req)) 
            self.safe_obj.set_buying_power_model(CfdSimulationBuyingPowerModel(self.uup_margin_req))
            
            # Neue akkurate Zins-Logik statt der alten apply_financing
            self.schedule.on(self.date_rules.every_day(self.qqq), 
                             self.time_rules.before_market_close(self.qqq, 1), 
                             self.apply_ibkr_interest_model)

        # 2. STRATEGIE-LIMITS
        self.initial_leverage = 3   
        self.max_leverage = 6       
        self.max_bp_utilization = 0.5 
        
        self.daily_growth_target = 0.03/4
        self.qqq_target_value = 0            
        self.invested_once = False
        
        self.total_financing_costs = 0
        self.total_interest_earned = 0

        #self.schedule.on(self.date_rules.every(DayOfWeek.MONDAY, DayOfWeek.FRIDAY), 
        #                 self.time_rules.after_market_open(self.qqq, 60), 
        #                 self.rebalance)
        self.schedule.on(self.date_rules.week_start(self.qqq), 
                         self.time_rules.after_market_open(self.qqq, 60), 
                         self.rebalance)

    def apply_ibkr_interest_model(self):
        """Akkurate Simulation der IBKR Kosten für CFDs und Cash"""
        if self._is_live: return

        # 1. Schritt: NAV und Skalierung (Schritt 2 der IBKR Anleitung)
        nav = self.portfolio.total_portfolio_value
        nav_factor = min(1.0, max(0, nav / 100000.0))
        
        # 2. Schritt: Exposure bestimmen (Bruttowert der CFDs)
        total_cfd_exposure = 0
        for kvp in self.portfolio:
            if kvp.value.invested:
                total_cfd_exposure += abs(kvp.value.holdings_value)

        # 3. Schritt: Zinsberechnung
        
        # A) CFD FINANCING (KOSTEN)
        # IBKR verlangt BM + 1.5% auf das gesamte Long-Exposure
        daily_cfd_cost = (total_cfd_exposure * self.borrow_rate) / 360
        
        # B) GUTHABENZINSEN (ERTRAG)
        # Da wir CFDs handeln, bleibt unser Kapital (NAV) als Guthaben stehen.
        # IBKR zahlt BM - 0.5% auf Guthaben über 10.000 USD, skaliert mit dem nav_factor.
        daily_credit_earned = 0
        if nav > 10000:
            taxable_cash = nav - 10000
            daily_credit_earned = (taxable_cash * self.credit_rate * nav_factor) / 360

        # C) MARGIN-SCHULDEN (Nur falls NAV negativ - extrem selten/Margin Call Risiko)
        daily_margin_debt_cost = 0
        if nav < 0:
            daily_margin_debt_cost = (abs(nav) * self.borrow_rate) / 360

        # 4. Schritt: Buchung
        net_daily_interest = daily_credit_earned - daily_cfd_cost - daily_margin_debt_cost
        self.portfolio.cash_book["USD"].add_amount(net_daily_interest)
        
        # Statistik & Zielwertanpassung
        self.total_financing_costs += daily_cfd_cost
        self.total_interest_earned += daily_credit_earned
        
        if self.invested_once:
            # Wir ziehen nur die Kosten vom Zielwert ab, nicht die Zinserträge, 
            # um konservativ zu bleiben, oder das Netto-Ergebnis:
            self.qqq_target_value += net_daily_interest 

        if self.time.day % 10 == 0:
            self.debug(f"NAV: {nav:.0f} | Exposure: {total_cfd_exposure:.0f} | Net Int: {net_daily_interest:.2f}")

    def initial_entry(self):
        self.qqq_target_value = self.portfolio.total_portfolio_value * self.initial_leverage
        self.rebalance()
        self.invested_once = True

    def on_data(self, data):
        if not self.invested_once and not self.is_warming_up:
            self.initial_entry()

    def rebalance(self):
        if not self.invested_once or self.is_warming_up: return

        port_val = self.portfolio.total_portfolio_value
        if port_val <= 0: return

        # --- TEIL 1: QQQ (Leader) ---
        target_qqq_weight = self.qqq_target_value / port_val
        actual_qqq_weight = min(target_qqq_weight, self.max_leverage)
        
        used_bp_qqq = actual_qqq_weight * self.qqq_margin_req
        
        # --- TEIL 2: UUP (Filler) ---
        available_bp_for_safe = max(0, self.max_bp_utilization - used_bp_qqq)
        actual_safe_weight = available_bp_for_safe / self.uup_margin_req

        # --- TEIL 3: AUSFÜHRUNG ---
        self.set_holdings(self.qqq, actual_qqq_weight)
        self.set_holdings(self.safe, actual_safe_weight)

        # Zielwert-Fortschreibung
        self.qqq_target_value *= (1 + self.daily_growth_target)
        
    def on_end_of_algorithm(self):
        if not self._is_live:
            self.log(f"Financing Paid: {self.total_financing_costs:.2f}")
            self.log(f"Interest Earned: {self.total_interest_earned:.2f}")