Overall Statistics
Total Orders
6788
Average Win
0.14%
Average Loss
-0.15%
Compounding Annual Return
-6.698%
Drawdown
59.800%
Expectancy
-0.128
Start Equity
10000000.00
End Equity
4349101.99
Net Profit
-56.509%
Sharpe Ratio
-0.608
Sortino Ratio
-0.727
Probabilistic Sharpe Ratio
0.000%
Loss Rate
55%
Win Rate
45%
Profit-Loss Ratio
0.92
Alpha
-0.053
Beta
-0.092
Annual Standard Deviation
0.101
Annual Variance
0.01
Information Ratio
-0.828
Tracking Error
0.182
Treynor Ratio
0.665
Total Fees
$111767.45
Estimated Strategy Capacity
$0
Lowest Capacity Asset
6L YONRSAUV1BSX
Portfolio Turnover
8.66%
Drawdown Recovery
41
# region imports
from AlgorithmImports import *
from QuantConnect.DataSource import Fred
from scipy.optimize import minimize
# endregion


class FuturesAndGlobalMacroAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2013, 1, 1)
        self.set_end_date(2024, 12, 31)
        self.set_cash(10000000)
        # Seed daily prices so the first rebalance (08:00 of the first month's
        # first trading day) can size equity/FX/future positions from a live
        # price instead of receiving a zero-price security.
        self.set_warmup(30, Resolution.DAILY)
        # Do not silently drop small-weight legs at sizing.
        self.settings.min_absolute_portfolio_target_percentage = 0
        self.settings.minimum_order_margin_portfolio_percentage = 0

        # Macro-variable publication lags (months) applied to each variable's
        # reference period; GDP reference period is the last month of the quarter.
        self._vars = ("GDP", "CPI", "RATE", "REER")
        self._lags = {"GDP": 6, "CPI": 3, "RATE": 1, "REER": 1}
        self._quarterly = {"GDP": True, "CPI": False, "RATE": False,
                           "REER": False}

        # FRED id per (macro region, variable). A value of None means the
        # series is not available on the platform for that region; the region's
        # vote for that variable then contributes 0 at the vote stage.
        # EA aggregates DE/FR/IT. CN GDP / CP CN and RU, IN GDP are unavailable.
        self._macro_ids = {
            "AR": {"GDP": "NGDPRSAXDCARQ", "CPI": None, "RATE": None,
                   "REER": "RBARBIS"},
            "AU": {"GDP": "NGDPRSAXDCAUQ", "CPI": None,
                   "RATE": "IRSTCI01AUM156N", "REER": "RBAUBIS"},
            "BR": {"GDP": "NGDPRSAXDCBRQ", "CPI": "CPALTT01BRM657N",
                   "RATE": "IRSTCI01BRM156N", "REER": "RBBRBIS"},
            "CA": {"GDP": "NGDPRSAXDCCAQ", "CPI": "CPALTT01CAM657N",
                   "RATE": "IRSTCI01CAM156N", "REER": "RBCABIS"},
            "CN": {"GDP": None, "CPI": "CPALTT01CNM657N",
                   "RATE": "IRSTCI01CNM156N", "REER": "RBCNBIS"},
            "EA": {"GDP": "NGDPRSAXDCESQ", "CPI": "CPALTT01EEM657N",
                   "RATE": "ECBMRRFR", "REER": "RBESBIS"},
            "IN": {"GDP": None, "CPI": "CPALTT01INM657N",
                   "RATE": "IRSTCI01INM156N", "REER": "RBINBIS"},
            "ID": {"GDP": "NGDPRSAXDCIDQ", "CPI": "CPALTT01IDM657N",
                   "RATE": "IRSTCI01IDM156N", "REER": "RBIDBIS"},
            "JP": {"GDP": "NGDPRSAXDCJPQ", "CPI": "CPALTT01JPM657N",
                   "RATE": "IRSTCI01JPM156N", "REER": "RBJPBIS"},
            "MX": {"GDP": "NGDPRSAXDCMXQ", "CPI": "CPALTT01MXM657N",
                   "RATE": "IRSTCI01MXM156N", "REER": "RBMXBIS"},
            "KR": {"GDP": "NGDPRSAXDCKRQ", "CPI": "CPALTT01KRM657N",
                   "RATE": "IRSTCI01KRM156N", "REER": "RBKRBIS"},
            "RU": {"GDP": None, "CPI": "CPALTT01RUM657N",
                   "RATE": "IRSTCI01RUM156N", "REER": "RBRUBIS"},
            "SA": {"GDP": "NGDPRSAXDCSAQ", "CPI": None, "RATE": None,
                   "REER": "RBSABIS"},
            "ZA": {"GDP": "NGDPRSAXDCZAQ", "CPI": "CPALTT01ZAM657N",
                   "RATE": "IRSTCI01ZAM156N", "REER": "RBZABIS"},
            "TR": {"GDP": "NGDPRSAXDCTRQ", "CPI": "CPALTT01TRM657N",
                   "RATE": "IRSTCI01TRM156N", "REER": "RBTRBIS"},
            "GB": {"GDP": "NGDPRSAXDCGBQ", "CPI": "CPALTT01GBM657N",
                   "RATE": "IRSTCI01GBM156N", "REER": "RBGBBIS"},
            "US": {"GDP": "NGDPRSAXDCUSQ", "CPI": "CPIAUCSL",
                   "RATE": "FEDFUNDS", "REER": "RBUSBIS"},
        }

        # Subscribe one FRED series per (region, variable) where available.
        # Unavailable combos stay as None -> vote contributes 0.
        self._macros = {}
        for region, ids in self._macro_ids.items():
            syms = {}
            for var in self._vars:
                if ids[var] is not None:
                    syms[var] = self.add_data(
                        Fred, ids[var], Resolution.DAILY).symbol
                else:
                    syms[var] = None
            self._macros[region] = syms

        self._dir = {"GDP": {"EQ": +1, "BO": -1, "FX": +1},
                     "CPI": {"EQ": -1, "BO": -1, "FX": +1},
                     "RATE": {"EQ": -1, "BO": -1, "FX": +1},
                     "REER": {"EQ": -1, "BO": +1, "FX": +1}}

        # Macro-sleeve assets carrying a macro country: EQ(19) + BO(1) + FX(13)
        # = 33. Tuples: (ticker, class, region, sub_type)
        #   sub_type: eq=equity/ETF, fx=forex spot, fut=continuous future
        self._assets = [
            # Equities (country ETF -> macro country)
            ("ARGT", "EQ", "AR", "eq"), ("EWA", "EQ", "AU", "eq"),
            ("EWZ", "EQ", "BR", "eq"), ("EWC", "EQ", "CA", "eq"),
            ("MCHI", "EQ", "CN", "eq"), ("EWQ", "EQ", "EA", "eq"),
            ("EWG", "EQ", "EA", "eq"), ("INDA", "EQ", "IN", "eq"),
            ("EIDO", "EQ", "ID", "eq"), ("EWI", "EQ", "EA", "eq"),
            ("EWJ", "EQ", "JP", "eq"), ("EWW", "EQ", "MX", "eq"),
            ("EWY", "EQ", "KR", "eq"), ("ERUS", "EQ", "RU", "eq"),
            ("KSA", "EQ", "SA", "eq"), ("EZA", "EQ", "ZA", "eq"),
            ("TUR", "EQ", "TR", "eq"), ("EWU", "EQ", "GB", "eq"),
            ("SPY", "EQ", "US", "eq"),
            # Bond (US 10Y future)
            ("ZN", "BO", "US", "fut"),
            # FX spot pairs
            ("AUDUSD", "FX", "AU", "fx"), ("USDCAD", "FX", "CA", "fx"),
            ("USDCNY", "FX", "CN", "fx"), ("EURUSD", "FX", "EA", "fx"),
            ("GBPUSD", "FX", "GB", "fx"), ("USDINR", "FX", "IN", "fx"),
            ("USDJPY", "FX", "JP", "fx"), ("USDMXN", "FX", "MX", "fx"),
            ("USDSAR", "FX", "SA", "fx"), ("USDTRY", "FX", "TR", "fx"),
            ("USDZAR", "FX", "ZA", "fx"),
            # FX futures (BRL, RUB)
            ("6R", "FX", "BR", "fut"), ("6L", "FX", "RU", "fut"),
        ]

        # Commodity continuous-future feeds that exist on the platform (re-
        # verified by data-availability probe). Commodities carry NO macro
        # country (spec section 4): trend sleeve only.
        self._commodities = [
            "BZ", "HG", "ZC", "GF", "HO", "GC", "KE", "HE",
            "LE", "NG", "PL", "SI", "ZM", "ZL", "ZS", "SB",
            "RB", "ZW", "CL",
        ]

        # Trend sleeve = ALL assets (EQ + BO + FX + COM). Every instrument is
        # subscribed once here; macro sleeve reuses the same symbols.
        self._trend = {}
        for tkr, cls, reg, sub in self._assets:
            self._trend[tkr] = self._subscribe(tkr, sub)
        for tkr in self._commodities:
            self._trend[tkr] = self._subscribe(tkr, "fut")

        self.schedule.on(
            self.date_rules.month_start("SPY"),
            self.time_rules.at(8, 0),
            self._run_trend)

    def _subscribe(self, tkr: str, sub: str):
        if sub == "eq":
            return self.add_equity(tkr, Resolution.DAILY).symbol
        if sub == "fx":
            return self.add_forex(tkr, Resolution.DAILY).symbol
        # Continuous future, backwards-ratio normalization (roll-gap-adjusted).
        return self.add_future(
            tkr, Resolution.DAILY,
            data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO).symbol

    def _shift_month(self, t, n: int):
        total = t[0] * 12 + (t[1] - 1) + n
        return (total // 12, total % 12 + 1)

    def _changes(self, key: str, t):
        """Reference-period changes of each macro variable for a region at
        decision time t, from the two most recent observations."""
        out = {}
        for var in self._vars:
            if self._macros[key][var] is None:
                out[var] = 0.0
                continue
            aref = self._shift_month((t[0], t[1]), -self._lags[var])
            end = datetime(aref[0], aref[1], 1)
            start = datetime(aref[0], aref[1] - 8, 1) if aref[1] > 8 else \
                datetime(aref[0] - 1, aref[1] + 4, 1)
            bars = list(self.history[Fred](
                self._macros[key][var], start, end, Resolution.DAILY))
            usable = [b for b in bars
                      if self._ref_period(b, self._quarterly[var]) <= aref]
            usable.sort(key=lambda b: b.end_time)
            if len(usable) < 2:
                out[var] = 0.0
                continue
            cur = usable[-1]
            prev = usable[-2]
            if var == "REER":
                ch = (cur.value - prev.value) / prev.value if prev.value else 0.0
            else:
                ch = cur.value - prev.value
            out[var] = ch
        return out

    def _ref_period(self, b, quarterly: bool):
        """Reference (year, month) of a macro observation. For quarterly
        series it is the LAST month of the quarter, so a FRED 'Q' bar stamped
        at quarter-end month+1 maps back one month."""
        if quarterly:
            m = b.end_time.month - 1
            y = b.end_time.year
            if m == 0:
                m = 12
                y -= 1
            return (y, m)
        return (b.end_time.year, b.end_time.month)

    def _sign_int(self, ch: float) -> int:
        return 1 if ch > 0 else (-1 if ch < 0 else 0)

    def _trend_signal(self, sym, t):
        """Monthly trend signal S^TF for an asset at decision month t.
        Returns None if not yet eligible (fewer than 36 complete monthly
        returns ending at t). Uses month-end-sampled monthly returns."""
        start = datetime(self._shift_month(t, -60)[0],
                         self._shift_month(t, -60)[1], 1)
        # History end is exclusive; request one month past the decision month
        # so the decision month's close is included, then filter down to it.
        ey, em = self._shift_month(t, 1)
        end = datetime(ey, em, 1)
        h = self.history(sym, start, end, Resolution.DAILY)
        if h is None or len(h) == 0:
            return None
        c = h["close"]
        c.index = c.index.get_level_values("time")
        mc = c.groupby(c.index.to_period("M")).last()
        mc = mc[mc.index <= pd.Period("%d-%02d" % (end.year, end.month))]
        ret = mc.pct_change().dropna()
        ret_t = ret.iloc[-36:]
        if len(ret_t) < 36:
            return None
        sigma36 = ret_t.std(ddof=1)
        if sigma36 == 0 or np.isnan(sigma36):
            return None
        out = {"sigma36": sigma36, "n_ret": len(ret)}
        for m in (1, 3, 12):
            R = mc.iloc[-1] / mc.iloc[-1 - m] - 1.0
            s = R / (sigma36 * np.sqrt(m))
            out["R%d" % m] = R
            out["s%d" % m] = s
        out["s1c"] = max(-2.0, min(2.0, out["s1"]))
        out["s3c"] = max(-2.0, min(2.0, out["s3"]))
        out["s12c"] = max(-2.0, min(2.0, out["s12"]))
        out["stf"] = (out["s1c"] + out["s3c"] + out["s12c"]) / 3.0
        return out

    def _monthly_ret(self, sym, t, n: int):
        """Month-end-sampled monthly returns ending at decision month t.
        Returns a numpy array of the last n complete monthly returns, or None
        if fewer than n are available."""
        start = datetime(self._shift_month(t, -(n + 24))[0],
                         self._shift_month(t, -(n + 24))[1], 1)
        ey, em = self._shift_month(t, 1)
        end = datetime(ey, em, 1)
        h = self.history(sym, start, end, Resolution.DAILY)
        if h is None or len(h) == 0:
            return None
        c = h["close"]
        c.index = c.index.get_level_values("time")
        mc = c.groupby(c.index.to_period("M")).last()
        mc = mc[mc.index.astype(str) <= "%d-%02d" % (t[0], t[1])]
        ret = mc.pct_change().dropna()
        return ret.iloc[-n:].values if len(ret) >= n else None

    def _erc_weights(self, syms, t):
        """Equal-risk-contribution base weights via SLSQP over the last 36
        month-end-sampled monthly returns ending at decision month t.
        Returns (cov, w, rc) or None if any asset lacks 36 monthly returns."""
        rets = [self._monthly_ret(s, t, 36) for s in syms]
        if any(r is None for r in rets):
            return None
        R = np.array(rets)           # n x 36
        cov = np.cov(R)              # n x n sample covariance
        n = len(syms)
        w0 = np.ones(n) / n

        def obj(wt):
            wv = np.array(wt)
            sig2 = wv @ cov @ wv
            if sig2 == 0:
                return 1e9
            mr = cov @ wv
            rc = wv * mr / sig2
            return float(np.sum((rc - 1.0 / n) ** 2))

        cons = {"type": "eq", "fun": lambda wv: float(np.sum(wv) - 1.0)}
        bnds = [(0.0, 1.0)] * n
        res = minimize(obj, w0, method="SLSQP", bounds=bnds,
                       constraints=cons, options={"ftol": 1e-14})
        w = np.array(res.x)
        sig2 = float(w @ cov @ w)
        mr = cov @ w
        rc = w * mr / sig2
        return cov, w, rc

    def _trend_weight_pass(self, t):
        """Full trend-sleeve weighting pass at decision month t.

        Returns (assets, w, sp, wfin) where assets is the list of eligible
        tickers, w the ERC base weights, sp the predicted annual volatility,
        and wfin the vol-targeted final weights. None if the ERC step fails
        (unexpected, since eligibility was pre-checked).
        """
        assets = []
        sigs = []
        for tkr, sym in self._trend.items():
            sig = self._trend_signal(sym, t)
            if sig is None:
                continue
            if self._monthly_ret(sym, t, 36) is None:
                continue
            assets.append(tkr)
            sigs.append(sig["stf"])
        syms = [self._trend[a] for a in assets]
        out = self._erc_weights(syms, t)
        if out is None:
            return None
        cov, w, rc = out
        # w~ = S^TF_i * w^RP_i ; normalise by sum of absolute weights
        wt = [w[i] * sigs[i] for i in range(len(assets))]
        abssum = sum(abs(v) for v in wt)
        wb = [v / abssum for v in wt]
        wb_arr = np.array(wb)
        # predicted annual volatility, same 36-month covariance
        sp = float(np.sqrt(12.0 * (wb_arr @ cov @ wb_arr)))
        sigma_target = 0.10
        wfin = [v * (sigma_target / sp) for v in wb]
        return assets, w, sp, wfin

    def _macro_weight_pass(self, t):
        """Full macro-sleeve weighting pass at decision month t over the
        33 macro assets (EQ + BO + FX). Returns (assets, w, sp, wfin) where
        assets is the eligible tickers, w the ERC base weights, sp the
        predicted annual volatility and wfin the vol-targeted final weights.
        Returns None if the ERC step fails (unexpected)."""
        assets = []
        sigs = []
        ch_cache = {}
        for a in self._assets:
            tkr, cls, reg, sub = a
            if self._monthly_ret(self._trend[tkr], t, 36) is None:
                continue
            if reg not in ch_cache:
                ch_cache[reg] = self._changes(reg, t)
            ch = ch_cache[reg]
            s = 0.0
            for var in self._vars:
                s += self._dir[var][cls] * self._sign_int(ch[var])
            assets.append(tkr)
            sigs.append(s)
        syms = [self._trend[a] for a in assets]
        out = self._erc_weights(syms, t)
        if out is None:
            return None
        cov, w, rc = out
        wt = [w[i] * sigs[i] for i in range(len(assets))]
        abssum = sum(abs(v) for v in wt)
        wb = [v / abssum for v in wt]
        wb_arr = np.array(wb)
        sp = float(np.sqrt(12.0 * (wb_arr @ cov @ wb_arr)))
        sigma_target = 0.10
        wfin = [v * (sigma_target / sp) for v in wb]
        return assets, w, sp, wfin

    def _blend_pass(self, t):
        """Compute the blended final weights at decision month t:
            w^final_i = 0.8 * w^TF_i + 0.2 * w^GM_i
        over all eligible assets (union of trend-eligible and macro-eligible).
        For an asset in only one sleeve the other sleeve's weight is 0.
        Returns a dict ticker -> final weight."""
        out = {}
        # Trend sleeve weights
        trend_res = self._trend_weight_pass(t)
        if trend_res is not None:
            tassets, _tw, _tsp, twfin = trend_res
            for i, tk in enumerate(tassets):
                if abs(twfin[i]) > 1e-12:
                    out[tk] = 0.8 * twfin[i]
        # Macro sleeve weights
        macro_res = self._macro_weight_pass(t)
        if macro_res is not None:
            massets, _mw, _msp, mwfin = macro_res
            for i, tk in enumerate(massets):
                if abs(mwfin[i]) > 1e-12:
                    out[tk] = out.get(tk, 0.0) + 0.2 * mwfin[i]
        return out

    def _is_futures(self, tkr: str) -> bool:
        """True for futures assets (commodities, the ZN bond future, and the
        6R/6L FX futures). Futures are sized in integer contracts; bodies."""
        if tkr in self._commodities or tkr in ("ZN", "6R", "6L"):
            return True
        return False

    def _current_decision_month(self):
        """Decision month = the just-completed month (rebalance fires on the
        first trading day of the next month at 08:00)."""
        return self._shift_month((self.time.year, self.time.month), -1)

    def _rebalance(self) -> None:
        """Monthly rebalance: set the book to the blended w^final weights for
        the decision month.

        Futures (commodities, the ZN bond future, and the 6R/6L FX futures) are
        sized as integer contracts by notional (the method-mandated instrument).
        Equities and FX spot are sized to their target weights with set_holdings
        so the WHOLE non-futures book is sized together and clamped to buying
        power; hand-rolling sequential market orders exhausts free margin on a
        large dollar-neutral book and rejects legs.
        """
        t = self._current_decision_month()
        blend = self._blend_pass(t)
        placed = []
        # Futures: integer-contract sizing by notional.
        for tkr in self._trend:
            if not self._is_futures(tkr):
                continue
            sym = self._trend[tkr]
            w = blend.get(tkr, 0.0)
            sec = self.securities[sym]
            contract = sec.mapped
            price = self.securities[contract].price
            mult = self.securities[contract].symbol_properties.contract_multiplier
            if price and mult:
                target = round(self.portfolio.total_portfolio_value * w /
                               (price * mult))
            else:
                target = 0
            cur = self.portfolio[contract].quantity
            delta = target - cur
            if delta != 0:
                self.market_order(contract, delta)
                side = "BUY" if delta > 0 else "SELL"
                placed.append("%s %s %d" % (tkr, side, delta))
        # Equity/FX legs: size the whole non-futures book together, skipping
        # any security that has not yet received a usable price (e.g. KSA,
        # which launched in 2015, and any session whose data has not yet
        # arrived at the 08:00 rebalance). Only securities with an accurate,
        # non-zero price go into the set_holdings targets.
        non_futures = []
        for tkr in self._trend:
            if self._is_futures(tkr):
                continue
            sec = self.securities[self._trend[tkr]]
            if sec.has_data and sec.price and sec.price > 0:
                non_futures.append(tkr)
        targets = [
            PortfolioTarget(self.securities[self._trend[tkr]].symbol,
                            blend.get(tkr, 0.0))
            for tkr in non_futures
        ]
        self.set_holdings(targets, liquidate_existing_holdings=False)
        # Diagnostic: report the size of the blended book being targeted.
        gross_long = sum(max(blend.get(t, 0.0), 0.0) for t in self._trend)
        gross_short = sum(max(-blend.get(t, 0.0), 0.0) for t in self._trend)
        msg = "; ".join(placed)
        self.log("REBALANCE t=%s placed=%d :: %s | GL=%.3f GS=%.3f"
                 % (t, len(placed), msg, gross_long, gross_short))

    def _run_trend(self) -> None:
        """Monthly rebalance event. Skip while warming up so no orders are
        placed before real trading begins."""
        if self.IsWarmingUp:
            return
        self._rebalance()

    def on_data(self, data: Slice) -> None:
        pass
# region imports
from AlgorithmImports import *
# endregion

SPEC = "# Futures and Global Macro — Vol-Adjusted Multi-Horizon Trend plus Directional Macro Votes with Risk-Parity Scaling\n\n**Deutsche Bank \"Academic Insights\" (AI October 2025), item #111 — \"Futures, ETFs & OTC — Macro #2: Futures and Global Macro\" (p.312).**\n**Underlying paper:** Aidan Vyas, *\"Evaluating the Performance of Systematic Trend-Following and Global Macro Strategies\"* (November 13, 2023), SSRN 4745633. Monthly data: Bloomberg total-return and price series for 74 assets in four classes, IMF International Financial Statistics macro series; the paper does not disclose explicit sample start/end dates. No public author code.\n\nImplement the **full method** below: two sleeves — (i) a time-series trend-following sleeve over all four asset classes, whose per-asset weight is the average of truncated volatility-adjusted 1-, 3-, and 12-month returns, and (ii) a global-macro sleeve over equities, government bonds, and currencies, whose per-asset weight is the sum of four directional ±1 votes driven by lagged changes in GDP growth, CPI inflation, the policy interest rate, and the real effective exchange rate of the asset's country — each sleeve refined by an equal-risk-contribution overlay on a rolling 3-year covariance and scaled to a volatility target, then blended 80% trend / 20% macro. Do not build a reduced version. Five points where the Deutsche Bank write-up deviates from the paper — follow the paper on all five:\n\n1. **No cross-sectional ranking.** The write-up says the portfolio is \"long the top and short the bottom assets in each asset class.\" The paper never ranks assets against one another: both sleeves are time-series constructions in which each asset receives its own signed weight from its own price history (trend) or its own country's macro changes (macro).\n2. **No single aggregated signal.** The write-up describes one book built from \"the aggregate of signals from trend following and global macro.\" The paper builds two separate portfolios — trend and macro, each with its own risk-parity pass — and blends the finished portfolios 80%/20%.\n3. **Four macro variables, not three.** The write-up lists GDP growth, inflation, and interest rates plus \"inter-relationships between the asset classes.\" The paper's fourth variable is each country's CPI-based real effective exchange rate, treated exactly like the other three: its 1-month-lagged relative change votes on the country's equities, bonds, and currency.\n4. **The macro sleeve excludes commodities.** The write-up implies the macro signal spans the whole universe. The paper's global-macro portfolio trades only equity, government-bond, and currency assets; commodities appear in the trend sleeve only.\n5. **Risk parity, not simple vol-weighting.** The write-up says positions are \"volatility-weighted.\" The paper computes covariance-based equal-risk-contribution base weights (SLSQP optimization on a rolling 3-year window), incorporates the sleeve's signal weights, normalizes, and scales to a target annual volatility.\n\n---\n\n## Structured brief (for the QC Assistants)\n\n**Concept.** Markets trend: an asset's own volatility-adjusted returns over the past 1, 3, and 12 months predict its next-month return, and the paper's central refinement is that the *magnitude* of the past move carries information beyond its sign — so each asset's trend weight is a continuous, truncated risk-adjusted return rather than a binary long/short. Macro conditions migrate into asset prices with a delay: accelerating growth lifts a country's equities and currency and hurts its bonds, rising inflation hurts equities and bonds but strengthens the currency, rate hikes hurt equities and bonds but strengthen the currency, and real-exchange-rate appreciation hurts equities, helps bonds, and continues in the currency — so signed votes on lagged macro changes position a second, largely independent book. Each sleeve's signal weights are disciplined by an equal-risk-contribution overlay and a volatility target, and the 80/20 blend of the two sleeves diversifies both against each other and against the equity market.\n\n**Investment universe.** The paper's 74 instruments in four classes (Appendix A.1 of the paper; full lists in section 1 of the implementation specifics): 19 MSCI country equity total-return indices spanning developed and emerging G20 members; 10 generic first-contract 10-year government bond series (Australia, Canada, China, France, Germany, Italy, Japan, Mexico, United Kingdom, United States); 16 USD currency pairs quoted so that a positive return is an appreciation of the non-USD currency against the USD; and 29 commodity total-return sub-indices covering energy, metals, grains, softs, and livestock.\n\n**Signal.** *Trend (all 74 assets):* for lookbacks $m \\in \\{1,3,12\\}$ months, the risk-adjusted signal is the compounded $m$-month return divided by (rolling 36-month monthly-return volatility × $\\sqrt{m}$), truncated to $[-2, +2]$; the trend weight is the average of the three truncated signals. *Macro (equities, bonds, currencies — 45 assets):* for each asset, four votes of $\\pm 1$ — one per macro variable of the asset's country (GDP growth, lagged 6 months; CPI inflation, lagged 3 months; policy interest rate, lagged 1 month; CPI-based real effective exchange rate, lagged 1 month) — each vote equal to the sign of the variable's latest change multiplied by the fixed direction of the asset-class/variable relationship; the macro weight is the sum of the four votes.\n\n**Portfolio construction & rebalance.** Monthly. Each sleeve independently: compute equal-risk-contribution base weights over the sleeve's eligible assets from a rolling 36-month covariance of monthly returns; multiply each asset's base weight by its sleeve signal weight; normalize by the sum of absolute weights; scale the book so its predicted annual volatility (from the same covariance) equals the 10% target (the target level is an operationalization — the paper scales to a \"pre-defined target annual volatility\" without stating the number). Final holdings each month are 0.8 × trend-sleeve weights + 0.2 × macro-sleeve weights. Decisions run at 8AM Eastern on the first trading day of the month from data through the prior month-end; each instrument's order fills at the next opening of its own market at or after the decision time; positions are held unchanged until the next monthly rebalance.\n\n**Reference.** Vyas, Aidan, *Evaluating the Performance of Systematic Trend-Following and Global Macro Strategies*, SSRN 4745633 — §2 (data and asset lists), §3.1 and equations (1)–(3) (risk parity), §3.2 and equation (4) (trend signal), §3.3 (macro relationships, lags, and ±1 votes), §5.3 (macro sleeve scope), §6 (80/20 blend), Appendix A.1 (instrument enumeration).\n\n---\n\n## Implementation specifics from the source paper\n\n### 1. Instrument universe (paper Appendix A.1)\n\n**Country equity total-return series (19)** — the MSCI country equity indices of: Argentina, Australia, Brazil, Canada, China, France, Germany, India, Indonesia, Italy, Japan, Mexico, South Korea, Russia, Saudi Arabia, South Africa, Turkey, United Kingdom, United States.\n\n**10-year government bond series (10)** — generic first-contract government bond series of: Australia, Canada, China, France, Germany, Italy, Japan, Mexico, United Kingdom, United States.\n\n**Currency pairs (16)** — USD against: ARS (Argentina), AUD (Australia), BRL (Brazil), CAD (Canada), CNY (China), EUR (euro), GBP (United Kingdom), IDR (Indonesia), INR (India), JPY (Japan), KRW (South Korea), MXN (Mexico), RUB (Russia), SAR (Saudi Arabia), TRY (Turkey), ZAR (South Africa).\n\n**Commodity total-return sub-index series (29)** — Aluminum, Brent Crude, Cocoa, Coffee, Copper, Corn, Cotton, Feeder Cattle, Gas Oil, Gold, Kansas Wheat, Lead, Lean Hogs, Live Cattle, Natural Gas, Nickel, Orange Juice, Platinum, Silver, Soybean Meal, Soybean Oil, Soybeans, Sugar, Tin, ULS Diesel, Unleaded Gasoline, Wheat, WTI Crude Oil, Zinc.\n\n### 2. Monthly return construction\n\n- All signals and covariances run on **month-end-sampled monthly returns**.\n- Equity and commodity series are total returns (income reinvested). Bond and currency series are price returns from the paper's price-history sourcing; in particular the currency series are spot exchange-rate returns only — the paper deliberately excludes carry and interest-rate differentials.\n- **Currency orientation:** every pair's return is transformed so that a **positive return means the non-USD currency appreciated** against the USD; a USD-base quote is inverted before computing returns.\n\n### 3. Trend-following sleeve signal (paper §3.2, equation (4))\n\nFor each asset $i$ at month-end $t$ and each lookback $m \\in \\{1, 3, 12\\}$ months:\n\n$$\ns^{(m)}_{i,t} \\;=\\; \\frac{R^{(m)}_{i,t}}{\\hat{\\sigma}_{i,t}\\,\\sqrt{m}}\n$$\n\nwhere $R^{(m)}_{i,t}$ is the compounded return over months $t{-}m{+}1$ through $t$, and $\\hat{\\sigma}_{i,t}$ is the standard deviation of the asset's monthly returns over the rolling 36-month window $t{-}35$ through $t$.\n\n- **Truncation:** each $s^{(m)}_{i,t}$ is truncated to the interval $[-2, +2]$ (values beyond the bound are set to the bound), per the paper's outlier control.\n- **Trend weight:** the average of the three truncated signals,\n\n$$\nS^{TF}_{i,t} \\;=\\; \\tfrac{1}{3}\\!\\left(s^{(1)}_{i,t} + s^{(3)}_{i,t} + s^{(12)}_{i,t}\\right).\n$$\n\nAll 74 assets carry a trend weight.\n\n### 4. Global-macro sleeve signal (paper §3.3)\n\n**Macro variables (per country, from the IMF International Financial Statistics families the paper names).** For each country $k$:\n\n- **GDP growth $g_k$:** growth of real, seasonally adjusted GDP in domestic currency. *Operationalization:* growth is measured quarter-over-quarter on successive quarterly observations (the paper procures the seasonally adjusted series, whose natural growth reading is quarter-over-quarter, but does not state the growth window).\n- **CPI inflation $\\pi_k$:** the percentage change of the all-items consumer price index. *Operationalization:* inflation is measured year-over-year (the standard reading of \"percentage alteration in the general price level over time,\" and one that needs no seasonal adjustment; the paper does not state the window).\n- **Policy interest rate $r_k$:** the central-bank policy rate (percent per annum); where a country publishes none, the monetary-policy-related interest rate — the paper's stated alternate series.\n- **Real effective exchange rate $E_k$:** the CPI-based real effective exchange rate index.\n\n**Changes.** Per the paper: for $g$, $\\pi$, and $r$ the change is the **difference between the current and previous values** of the variable (successive observations of each series — quarterly for $g$, monthly for $\\pi$ and $r$); for $E$ the change is the **relative change** $(E_{\\text{cur}} - E_{\\text{prev}})/E_{\\text{prev}}$ over successive monthly observations.\n\n**Publication lags.** At the rebalance decision for month $t$, the newest usable observation is lagged by the paper's first-investible-point convention: GDP through reference month $t{-}6$, CPI through $t{-}3$, policy rate and real effective exchange rate through $t{-}1$. The change is computed from the two most recent usable observations.\n\n**Direction map $d_{c,v}$** (asset class $c$ × macro variable $v$; a positive entry means an increase in the variable is a vote to be long the asset):\n\n| Macro variable (change) | Lag | Equities | Government bonds | Currencies |\n|---|---|---|---|---|\n| GDP growth | 6 months | +1 | −1 | +1 |\n| CPI inflation | 3 months | −1 | −1 | +1 |\n| Policy interest rate | 1 month | −1 | −1 | +1 |\n| Real effective exchange rate | 1 month | −1 | +1 | +1 |\n\n**Votes and composite weight.** For asset $i$ of class $c(i)$ and country $k(i)$, each pairing casts a purely directional vote $d_{c(i),v} \\cdot \\operatorname{sign}(\\Delta x^{v}_{k(i)}) \\in \\{-1, +1\\}$, and the macro weight is the sum across the four variables:\n\n$$\nS^{GM}_{i,t} \\;=\\; \\sum_{v \\in \\{g,\\;\\pi,\\;r,\\;E\\}} d_{c(i),v}\\cdot \\operatorname{sign}\\!\\big(\\Delta x^{v}_{k(i),t}\\big) \\;\\in\\; \\{-4,\\dots,+4\\}.\n$$\n\n- **Scope:** only the 19 equity, 10 bond, and 16 currency assets carry macro weights; commodities do not enter this sleeve.\n- **Country mapping:** an equity or bond asset uses the macro variables of its own country; a currency pair uses those of the non-USD economy. *Operationalization:* the USD/EUR pair uses euro-area aggregate series (euro-area GDP, euro-area inflation, the ECB policy rate, the euro-area real effective exchange rate); the paper selects \"common countries from both datasets\" without addressing the shared-currency case.\n- **Edge rule (operationalization):** a pairing whose change is exactly zero, or whose country series is unavailable at that date, casts a vote of 0 — the paper defines only ±1 votes on signed changes.\n\n### 5. Risk-parity refinement, normalization, and volatility targeting (paper §3.1)\n\nEach sleeve is refined independently, over its own asset set (trend: all eligible assets of the 74; macro: the eligible assets of the 45), at each month-end $t$:\n\n1. **Covariance:** $\\hat{\\Sigma}_t$ = sample covariance of the sleeve assets' monthly returns over the rolling 3-year window (36 months) ending at $t$.\n2. **Equal-risk-contribution base weights:** with portfolio variance $\\sigma^2 = w^{\\top}\\hat{\\Sigma}_t w$ and risk contributions $RC_i = w_i(\\hat{\\Sigma}_t w)_i / \\sigma^2$, solve\n\n$$\n\\min_{w}\\; \\sum_{i=1}^{n}\\left(RC_i - \\frac{1}{n}\\right)^{2}\n\\qquad \\text{subject to} \\qquad \\sum_{i=1}^{n} w_i = 1,\n$$\n\nby Sequential Least Squares Quadratic Programming (SLSQP) initialized at uniform weights $w_i = 1/n$, per the paper. *Operationalization:* bound the weights at $w_i \\ge 0$ — the equal-risk-contribution solution reached from uniform initial weights is the long-only one, and the paper's base portfolio is a long-only risk-parity book.\n3. **Incorporate the sleeve's signal weights** ($S_{i,t}$ = $S^{TF}_{i,t}$ or $S^{GM}_{i,t}$), **normalize, and scale to the volatility target.** *Operationalization of the paper's \"incorporate any external weights, normalize the combined weights, and scale them\" (the paper states the steps without formulas):*\n\n$$\n\\tilde{w}_{i,t} = S_{i,t}\\; w^{RP}_{i,t}, \\qquad\n\\bar{w}_{i,t} = \\frac{\\tilde{w}_{i,t}}{\\sum_{j} \\lvert \\tilde{w}_{j,t} \\rvert}, \\qquad\nw_{i,t} = \\bar{w}_{i,t}\\cdot \\frac{\\sigma_{\\text{target}}}{\\hat{\\sigma}_{p,t}},\n\\qquad\n\\hat{\\sigma}_{p,t} = \\sqrt{12\\; \\bar{w}_t^{\\top} \\hat{\\Sigma}_t\\, \\bar{w}_t},\n$$\n\nwith $\\sigma_{\\text{target}} = 10\\%$ annual (operationalization: the paper's target level is undisclosed; 10% is the standard target of the multi-asset trend literature the paper builds on, per the Hurst, Ooi & Pedersen convention, SSRN 2993026).\n\n### 6. Sleeve blend, rebalance timing, and eligibility\n\n1. **Blend:** final holdings each month are $w^{\\text{final}}_{i,t} = 0.8\\, w^{TF}_{i,t} + 0.2\\, w^{GM}_{i,t}$ — the paper's 80% trend-following / 20% global-macro combined portfolio (an asset in both sleeves gets the sum of its two contributions).\n2. **Timing:** the decision runs at 8AM Eastern on the first trading day of each month using price data through the prior month-end close and macro observations lagged per section 4; each instrument's order fills at the next opening of its own market at or after the decision time; the book is held unchanged until the next monthly rebalance.\n3. **Eligibility (operationalization):** an asset enters a sleeve once it has 36 complete monthly returns (the requirement of the volatility and covariance windows, which subsumes the 12-month lookback); the equal-risk-contribution optimization each month runs on the assets eligible that month. An instrument whose series terminates (delisting or a permanently halted market) leaves the book at its final trading date and re-enters only per the same eligibility rule.\n\n### 7. Validation period\n\nThe final full-period validation backtest runs **January 1, 2013 through December 31, 2024**. (Platform histories for the futures-based sleeves begin around 2009 and the method's longest estimation window is 36 months, so 2013 is the earliest start at full strength; instruments whose data begins later enter the universe as their own 36-month histories complete.)\n\n---\n"