TradingView allocation research

Dual momentum: relative ranking plus absolute filters

Dual momentum combines two decisions: rank assets by past return, then require the selected asset to clear an absolute return hurdle before holding it. A relative winner can still lose money or trail cash. The teaching rule below holds the strongest of three risk assets only when its completed 12-month return exceeds the cash proxy return; otherwise it selects the cash proxy. It makes one monthly decision. The Pine example monitors that selection and does not simulate trades across assets.

See the decision guide

Relative momentum and absolute momentum answer different questions

Gary Antonacci describes relative momentum as comparing an asset with its peers and absolute momentum as examining its own excess return through time. His dual momentum work combines them. The distinction matters when all candidates are weak: ranking still produces a winner, while an absolute filter can reject that winner.

  • Relative strength here means a return comparison, not the RSI oscillator. A single-symbol momentum entry rule does not perform this cross-sectional selection.
  • This page specifies a custom three-asset, winner-filter variant. It is not an exact implementation of Antonacci's Global Equities Momentum (GEM). GEM has its own universe, absolute-signal reference and defensive allocation; its published figures cannot be assigned to this example.
MethodQuestionExample with returns of -2%, -5% and -8%
Relative momentumWhich risk asset performed best?The -2% asset ranks first. Ranking alone can leave the portfolio invested.
Absolute momentumDid an asset exceed a stated hurdle?With a +3% cash return hurdle, all three fail. No peer ranking is needed for this test.
Dual momentumDoes the relative winner also clear the hurdle?Rank first, reject the -2% winner, select cash.

Specify the universe, hurdle and monthly clock

For a reproducible baseline, label three distinct risk assets A, B and C and a separate cash substitute F. The monitor defaults to SPY, EFA, IWM and BIL as example USD-listed ETFs, not a recommended allocation. Use comparable dividend-adjusted monthly closes for every series. Freeze the universe before testing; changing it after observing returns changes the experiment.

  • A zero-return hurdle is a different absolute momentum strategy. If the winner returns +2% and the cash proxy returns +3%, the zero rule invests while this excess-return rule selects F.
  • A 1 percentage point margin means 0.01 added to the lookback return, not 1% per month or a 1% relative increase in the cash return. With a 3% cash return it sets H to 4%.
  • Use the same currency and calendar for this monitor. International exchange calendars, currency conversion, delistings and point-in-time universe changes need additional data controls. An ETF listed in the US can still contain foreign currency exposure.
RuleExact convention
ReturnR(i,m) = V(i,m) / V(i,m-L) - 1, using L = 12 completed calendar months and 13 valid month-end observations. V is a comparable return index or adjusted close.
Relative selectionChoose the largest R among A, B and C. Exact ties favor A, then B, then C. Do not rank rounded display values.
Absolute hurdleH = R(F,m) + b. The default margin b is zero percentage points over the full lookback window. Hold the winner only if R(winner,m) > H.
FallbackSelect F when the winner fails, including equality. F is a fund with its own return, costs and risks, not a guaranteed interest rate or a zero-return cash balance.
Decision timeAt the close of the first available daily chart bar in each new exchange-calendar month, using only the previous completed monthly observations. This deliberately waits one daily session after month-end.
Execution conventionFor a separate portfolio test, submit the frozen target after that decision and use the next executable session open. Hold the target until the next scheduled decision. The monitor itself places no orders.
Missing observationsRequire all four series to contain consecutive valid monthly observations and end in the previous calendar month. Otherwise mark the decision invalid and retain the previous target as a stale record; do not issue a new allocation.

Recompute the selection from four return indices

These invented index values show one 12-month decision. All intermediate month-end observations are assumed present. The margin is zero. The final column follows directly from V(now) / V(12 months ago) - 1; comparing the raw ending index levels would give the wrong ranking.

  • A wins the relative ranking, then passes because 8% > 3%. The target is 100% A. The +5 percentage point excess is a historical comparison, not an expected future return.
  • In an independent weak-market snapshot, ending values A = 102, B = 202, C = 76 and F = 103 produce +2%, +1%, -5% and +3%. A still wins, but 2% is below 3%, so the target is F.
  • If A and B both return +5%, A wins under the fixed tie rule. If the winner and F both return exactly +3%, the strict test fails and selects F. If any required observation is missing, none of these allocation results is valid.
AssetIndex 12 months agoLast completed month-endReturn and result
A100108108 / 100 - 1 = +8%; relative winner
B200210210 / 200 - 1 = +5%; second
C807676 / 80 - 1 = -5%; third
F100103103 / 100 - 1 = +3%; absolute hurdle

The fallback return belongs in the portfolio ledger

The cash proxy has two separate roles: its past return supplies the hurdle, and its future realized holding return affects equity when selected. A 3% trailing return does not mean it will earn 3% next month. Use actual dated returns in a portfolio test, or clearly label any assumed interest series.

  • The +0.30% is a fictional total return over this execution-to-execution holding interval. It is neither a current yield nor a historical BIL result. A zero-interest cash balance, under the same initial sale, ends at $9,990 because no fund purchase occurs.
  • If the selection stays F, do not charge a full sale and repurchase. When it switches, charge both sides. For whole shares, round purchases down and carry residual cash with its own interest assumption.
  • Adjusted returns already incorporate their specified distribution treatment. Do not add the same dividends again. Use executable unadjusted prices for fills and a consistent holdings, distribution and cash ledger; do not confuse adjusted chart prices with available transaction prices.
Hypothetical stepCalculation
Start at execution$10,000 in A; sell it at the reference price. Assume costs of 0.10% of each traded dollar, fractional shares and no price movement during execution.
Sell cost and proceeds$10,000 × 0.001 = $10; cash after sale = $9,990.
Buy F with fees reservedPurchased notional N solves N × 1.001 = $9,990, so N = $9,980.01998. Purchase cost = $9.98002.
F earns an assumed +0.30%$9,980.01998 × 1.003 = $10,009.96004 before the next rebalance. Net return from $10,000 is +0.0996004%.

Pine v6 monitor for a monthly dual momentum signal

Run this indicator on a standard 1D chart with the same US session calendar as the four inputs. It requests dividend adjustment explicitly, reads only completed monthly bars, and validates consecutive calendar months. The output is a target identifier and a status table. There is no strategy declaration, portfolio equity curve or multi-asset order simulation.

  • The monthly request uses close[1] and close[L+1] with lookahead_on. Both endpoints are already completed; removing those offsets would expose future month-end values on historical daily bars. An incomplete current month never enters the score.
  • Set the alert to once per bar close. The first loaded chart bar cannot create a decision. If warm-up finishes after the monthly decision, wait until the next month. Invalid decisions retain the old plotted identifier with an explicit stale status, not a new recommendation.
  • The code requests dividend adjustment, but availability depends on the instrument and feed. It cannot prove the vendor supplied a correct total-return series or detect an omitted final trading day within an otherwise present monthly bar. Check raw observations, adjustment availability and source timestamps before using a signal.
  • The table shows current completed-month scores and the separately dated last decision. Before the first daily bar of a new month closes, those scores can update while the last target still belongs to the prior decision. The next-session execution delay belongs in any comparison test.
Signal monitor only. F=0, A=1, B=2, C=3; blank before the first valid decision. Syntax validation does not establish data quality, returns or executable fills.pine
//@version=6
indicator("Dual momentum monthly monitor", overlay = false, max_bars_back = 120)
string assetA = input.symbol("AMEX:SPY", "Risk asset A (first tie priority)")
string assetB = input.symbol("AMEX:EFA", "Risk asset B")
string assetC = input.symbol("AMEX:IWM", "Risk asset C")
string fallback = input.symbol("AMEX:BIL", "Cash substitute F")
int months = input.int(12, "Lookback in completed months", minval = 1, maxval = 60)
float margin = input.float(0.0, "Hurdle margin (percentage points)", minval = 0.0) / 100.0
if not timeframe.isdaily or timeframe.multiplier != 1
    runtime.error("Use a standard 1D chart with matching US sessions.")
if assetA == assetB or assetA == assetC or assetB == assetC or fallback == assetA or fallback == assetB or fallback == assetC
    runtime.error("Use four distinct symbols.")

monthId(int stamp) =>
    year(stamp) * 12 + month(stamp)

monthlyPacket(int length) =>
    bool valid = true
    int lastMonth = monthId(time[1])
    for j = 1 to length + 1
        if na(close[j]) or close[j] <= 0 or na(time[j]) or monthId(time[j]) != lastMonth - j + 1
            valid := false
    float ret = valid ? close[1] / close[length + 1] - 1.0 : na
    [ret, lastMonth]

readAsset(string symbol, int length) =>
    string adjusted = ticker.modify(symbol, session = session.regular, adjustment = adjustment.dividends)
    request.security(adjusted, "1M", monthlyPacket(length), gaps = barmerge.gaps_off, lookahead = barmerge.lookahead_on, ignore_invalid_symbol = true)

[rA, mA] = readAsset(assetA, months)
[rB, mB] = readAsset(assetB, months)
[rC, mC] = readAsset(assetC, months)
[rF, mF] = readAsset(fallback, months)
int expectedMonth = monthId(time) - 1
bool ready = not na(rA) and not na(rB) and not na(rC) and not na(rF) and mA == expectedMonth and mB == expectedMonth and mC == expectedMonth and mF == expectedMonth
bool decisionBar = timeframe.change("1M") and bar_index > 0 and barstate.isconfirmed
var int selected = na
var string status = "Waiting for monthly decision"
var string decisionDate = "None"
if decisionBar
    decisionDate := str.format_time(time_close, "yyyy-MM-dd", syminfo.timezone)
    if ready
        int winner = 1
        float best = rA
        if rB > best
            winner := 2
            best := rB
        if rC > best
            winner := 3
            best := rC
        selected := best > rF + margin ? winner : 0
        status := selected == 0 ? "Valid: fallback selected" : "Valid: winner passes hurdle"
    else
        status := "INVALID: no new target; prior target is stale"

formatReturn(float value) =>
    na(value) ? "Missing" : str.tostring(value * 100.0, "#.####") + "%"
string targetName = na(selected) ? "None" : selected == 1 ? assetA : selected == 2 ? assetB : selected == 3 ? assetC : fallback
var table panel = table.new(position.top_right, 2, 8, border_width = 1)
if barstate.islast
    table.cell(panel, 0, 0, "Completed-month score")
    table.cell(panel, 1, 0, "Lookback return")
    table.cell(panel, 0, 1, assetA)
    table.cell(panel, 1, 1, formatReturn(rA))
    table.cell(panel, 0, 2, assetB)
    table.cell(panel, 1, 2, formatReturn(rB))
    table.cell(panel, 0, 3, assetC)
    table.cell(panel, 1, 3, formatReturn(rC))
    table.cell(panel, 0, 4, "F + margin")
    table.cell(panel, 1, 4, formatReturn(rF + margin))
    table.cell(panel, 0, 5, "Last decision: " + decisionDate)
    table.cell(panel, 1, 5, targetName)
    table.cell(panel, 0, 6, "Decision status")
    table.cell(panel, 1, 6, status)
    table.cell(panel, 0, 7, "Current monthly data")
    table.cell(panel, 1, 7, ready ? "Complete" : "Missing / stale")
plot(selected, "Last target: F=0, A=1, B=2, C=3", style = plot.style_stepline)
alertcondition(decisionBar and ready, "Monthly target ready", "Confirmed dual momentum target ready. Inspect the table; no orders placed.")

Test the allocation rule without borrowing a single-chart backtest

A Pine request for another symbol supplies data; it does not create a tradable position in that symbol in Strategy Tester. To evaluate this allocation, use a portfolio ledger with all four assets, actual fills, distributions and residual cash. A chart-symbol strategy can test a separate execution rule, but its report is not the return of this rotation.

  • Audit the worked positive, failed-hurdle, exact-tie and missing-data cases first. Export the monthly input panel and compare the recorded target with an independent calculation. Then check switches, unchanged holdings and delayed fills in the portfolio ledger.
  • Compare relative-only selection, the same universe with the absolute hurdle, and a declared fixed-allocation benchmark on identical dates. Keep costs and data conventions equal so a result is not merely a different cash-interest assumption.
  • Freeze a small lookback and hurdle experiment before examining a later evaluation period. Keep every attempted setting. Whipsaws near the hurdle increase turnover; longer lookbacks and monthly decisions can react late to crashes and miss sharp rebounds.
  • The cash substitute can lose value. A fallback changes exposure but supplies no loss floor. A concentrated winning asset can fall before the next decision, and a fixed present-day universe can embed survivorship and selection bias.

Use Pineify to inspect the rule before extending it

Pine Script AI Coding Agent can help edit the monitor, explain the monthly offsets and add diagnostics. Give it the exact universe, tie rule, margin units and invalid-data policy, then inspect the generated source and run syntax validation. The first useful output is an auditable signal record.

  • Strategy Optimizer compares exposed input combinations for TradingView strategies. This indicator produces no strategy report to optimize. Use the optimizer only for a separately defined chart-symbol strategy, and keep any portfolio allocation evaluation in its own validated model.
  • Changing the lookback, universe, cash substitute or rebalance clock creates a new research trial. Save those choices with the source, feed settings and results instead of treating the highest historical outcome as proof of future returns.
Where Pineify fits

Pine Script AI Coding Agent

Adapt the monthly selection monitor with explicit inputs and diagnostics. Review the return window, missing-data policy and generated Pine before interpreting a signal.

Also useful: Strategy Optimizer. Compare inputs for a separate TradingView chart-symbol strategy. The indicator above has no strategy report, and parameter search does not supply multi-asset portfolio accounting.

Build a dual momentum monitor

Frequently asked questions

Educational research, not investment advice. Values, returns and costs in the examples are hypothetical. No performance backtest is claimed. Momentum filters and cash substitutes can lose money; past returns do not guarantee future results.

Sources and verification

Published by Pineify. Pineify is an independent product and is not affiliated with TradingView. Sources support the technical explanations; examples do not establish trading performance.