| 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}")