Overall Statistics
Total Orders
183
Average Win
0.08%
Average Loss
-0.03%
Compounding Annual Return
2.768%
Drawdown
15.700%
Expectancy
1.919
Start Equity
100000
End Equity
111905.96
Net Profit
11.906%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
5.635%
Loss Rate
25%
Win Rate
75%
Profit-Loss Ratio
2.91
Alpha
-0.024
Beta
0.277
Annual Standard Deviation
0.06
Annual Variance
0.004
Information Ratio
-0.629
Tracking Error
0.138
Treynor Ratio
0
Total Fees
$183.00
Estimated Strategy Capacity
$430000.00
Lowest Capacity Asset
IBND UMQPTIBCU8V9
Portfolio Turnover
0.09%
# region imports
from AlgorithmImports import *
# endregion
from QuantConnect.Algorithm.Framework.Portfolio import PortfolioConstructionModel
from QuantConnect.Algorithm.Framework.Execution import ImmediateExecutionModel
from QuantConnect.Algorithm.Framework.Alphas import Insight, InsightDirection
from QuantConnect.Orders import OrderStatus
from datetime import timedelta

# ------------- NEW: 5-percent-per-signal portfolio model -------------
class FixedFractionPortfolioConstructionModel(PortfolioConstructionModel):
    """Always allocate fixed fraction of portfolio value per insight."""
    def __init__(self, fraction: float = 0.05):
        super().__init__()
        self.fraction = fraction

    def CreateTargets(self, algorithm, insights):
        targets = []
        for ins in insights:
            if ins.Direction == InsightDirection.Flat:
                continue
            direction = 1 if ins.Direction == InsightDirection.Up else -1
            weight = direction * self.fraction
            targets.append(PortfolioTarget.Percent(algorithm, ins.Symbol, weight))
        return targets
# --------------------------------------------------------------------

class MatureBondAndIndexETFsWithStops(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 1, 1)
        self.SetEndDate(2024, 2, 12)
        self.SetCash(100_000)
        self.SetTimeZone(TimeZones.NewYork)

        # ---------------- Universe ------------------------------------
        self.tickers = [
            # Bond / fixed-income ETFs
            "SHY","IEI","IEF","TLT","TIP","AGG","BND","LQD","HYG","IBND",
            # Major equity-index ETFs
            "SPY","QQQ","DIA","IWM","VTI"
        ]

        self.SL_mult = 0.5
        self.TP_mult = 2.0
        self.insightAtr = {}

        for tkr in self.tickers:
            self.AddEquity(tkr, Resolution.Daily)
        self.anchor = "SPY"

        # --------- << use the new 5 % model here >> -------------------
        self.SetPortfolioConstruction(FixedFractionPortfolioConstructionModel(0.05))
        self.SetExecution(ImmediateExecutionModel())
        # --------------------------------------------------------------

        self.Schedule.On(
            self.DateRules.EveryDay(self.anchor),
            self.TimeRules.AfterMarketClose(self.anchor, 1),
            self.GenerateInsights
        )

    # ================================================================
    def GenerateInsights(self):
        hist = self.History(self.tickers, 16, Resolution.Daily)
        if hist.empty:
            return

        insights = []
        for sym in self.tickers:
            if sym not in hist.index.get_level_values(0):
                continue
            df = hist.loc[sym].copy()
            if df.shape[0] < 16:
                continue

            # two up-days momentum signal
            r1 = df["close"].pct_change().iloc[-1]
            r2 = df["close"].pct_change().iloc[-2]
            if not (r1 > 0 and r2 > 0):
                continue

            prev_close = df["close"].shift(1)
            tr = pd.concat([
                    df["high"] - df["low"],
                    (df["high"] - prev_close).abs(),
                    (df["low"]  - prev_close).abs()
                 ], axis=1).max(axis=1)
            atr = tr.rolling(14).mean().shift(1).iloc[-1]
            if pd.isna(atr):
                continue

            self.insightAtr[sym] = atr
            insights.append(
                Insight.Price(sym, timedelta(days=1), InsightDirection.Up)
            )
            self.Log(f"{sym} insight @ {self.Time}  ATR14={atr:.4f}")

        if insights:
            self.EmitInsights(insights)

    # ================================================================
    def OnOrderEvent(self, oe):
        if oe.Status != OrderStatus.Filled or oe.FillQuantity <= 0:
            return
        sym = oe.Symbol
        atr = self.insightAtr.get(sym)
        if atr is None:
            return

        price = oe.FillPrice
        qty   = oe.FillQuantity
        sl = price - self.SL_mult * atr
        tp = price + self.TP_mult * atr

        self.StopMarketOrder(sym, -qty, sl)
        self.LimitOrder(sym, -qty, tp)
        self.Log(f"{sym} filled → SL {sl:.2f}  TP {tp:.2f}  qty={qty}")

    def OnEndOfAlgorithm(self):
        self.Log(f"Final Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}")