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