Free · TradingView

One free indicator that puts paid signal services and courses to shame — a live trade HUD with a single alert, sent straight to your phone at exactly the right moment.

Open Legacy is a single free Pine Script v6 indicator that works on any market — forex, indices, metals, crypto, you name it. It calculates fixed take-profit and stop-loss levels the moment a signal fires, plots the matching BUY / SELL labels and SL / TP lines directly on price, keeps a heads-up display panel showing the live status of the current signal, and automatically detects the right inverse-correlation benchmark for whatever you're charting — no second script or second chart required. It fires alerts the moment a target is hit or a new signal appears.

Fixed TP / SL levels Live HUD status panel Auto-detected correlation confirmation One alert, straight to your phone
XAUUSD · 15m · one example of any market
2,436.00 2,429.00 2,422.00 2,415.00 2,408.00 2,401.00 BUY SELL TP 2,420.30 (+$6.20) SL 2,403.30 (-$10.80) Open Legacy BUY ACTIVE TP Hit Odds 64.2% Fixed Entry ($) 2,414.10 Take Profit ($) 2,420.30 Stop Loss ($) 2,403.30 DXY Structure Confirmed ✅
Free tool, real risk. Open Legacy is a free indicator for TradingView that works on any market, not a signal service or trading advice. All trading carries risk of loss. The HUD's "TP Hit Odds" figure is an internal heuristic built from price distance and correlation with the auto-detected benchmark — it is a reference estimate, not a guarantee of outcome.
Start here

Quick Start

Five steps to a working chart. Everything below this section is detail you can come back to.

Open Legacy — manual signals

Signals plus a live HUD showing entry, take profit and stop loss.

1
Add the script. Copy it from Get the Script, paste into TradingView's Pine Editor, then Add to Chart.
2
Set your timezone. In settings, put your own IANA zone (e.g. Europe/Dublin) into Timezone (under 3. Display). Get this wrong and the market-closed status is wrong.
3
Pick a strategy and sensitivity. FVG Pullback or EMA Trend, starting on Balanced. Conservative gives fewer, higher-quality signals; Aggressive gives the most.
4
Wait for the candle to close. A signal only counts once its candle has finished. Acting mid-candle is the most common way to lose on a signal that never really existed.
5
Use the HUD numbers. Entry, TP and SL are locked when the signal fires — put those into your broker rather than guessing.
Get set upThree things to get right▼
DAY TO DAYCheat Sheet▼

Once it's set up, this is all you actually need. Everything else on this page is background you can come back to.

Every session

Four habits that matter more than any setting.

1
Wait for the candle to close. A label that appears mid-candle can vanish. Nothing counts until the candle is done.
2
Use the HUD numbers as they are. Entry, TP and SL lock when the signal fires — type those into your broker rather than adjusting them.
3
Check the structure rows. Strong with correlation confirmed is the cleanest setup; Weak or Invalid is the one to skip.
4
Sit out news and the midday lull. Two of the easiest losses to avoid entirely.
Quick reference
Answer
Best timeframe
15m
Starting sensitivity
Balanced
Starting strategy
FVG Pullback
Timezone format
Europe/Dublin, Asia/Kolkata
Signal didn't appear?
Candle hadn't closed
Weekly check
Scoreboard TP / SL tally
TWO PROVEN STRATEGIESEntry Methods▼

Two well-known strategies — the only ones that held up when tested on three months of real 1-minute data across gold, indices and BTC. One dropdown picks the strategy; one Sensitivity dial sets how picky it is. Open Legacy Bot uses exactly the same engine, so the same settings give the same signals.

Strategy
What it waits for
FVG Pullback
Buys the dip into an imbalance
A fair value gap is left when price moves so fast that a candle never overlaps the one two candles back. A signal fires when price pulls back into that gap and a candle closes back out in the gap's direction. Tiny gaps are ignored.
EMA Trend
Catches a new trend leg
Signals on the first candle where a fast EMA is above a slow EMA, price is above the fast EMA, and RSI is above 50 (all reversed for sells) — once per leg, not on every candle of the trend.
Sensitivity
What changes
Aggressive
Most signals
Every valid setup, with faster settings. More signals, more losers in the mix.
Balanced
About half as many
Only signals that agree with the 50 EMA trend on your chart.
Conservative
Fewest, best quality
Also needs the trend on a timeframe 4x your chart to agree, and real trend strength (ADX 20+).
01How to Install▼

One script, one chart, any market. Open Legacy runs entirely on whichever chart you put it on — there's nothing else to add.

Open the chart for the market you want to trade

Forex, indices, metals, crypto — Open Legacy auto-detects what you're charting and adapts to it.

Open the Pine Editor

Click Pine Editor in the bottom panel of the chart window.

Clear the default template

Select all the placeholder code in the editor and delete it, leaving a blank file.

Paste the script

Copy the full source for Open Legacy from the Get the Scripts section below and paste it in.

Click "Add to Chart"

The indicator compiles and appears on your chart — BUY/SELL labels, TP/SL lines, and the HUD panel all show up together, in one shot.

HUD not staying visible? If the HUD table gets tucked behind chart drawings or other indicators, right-click Open Legacy's name in the top-left of the chart, choose Visual Order, then Bring to Front.
One-Time SetupSet Your Timezone▼

One quick field makes the HUD run on your own clock instead of a default one.

After adding Open Legacy to your chart, open its settings (⚙️) → Inputs → 3. Display, and set Timezone to your own region — e.g. America/New_York, Europe/London, Asia/Tokyo. That keeps the market-open and market-closed status accurate to wherever you are.
02Key Settings▼

Ten settings in three groups. Everything else is handled for you.

Setting
What it controls
Entry Method
FVG Pullback / EMA Trend
Which strategy finds the signals. See Entry Methods above.
Sensitivity
Aggressive / Balanced / Conservative
How picky it is — more signals, or fewer and better ones.
Stop Loss / Take Profit (pips)
100 / 200
Where the HUD and the dashed lines put your SL and TP. Keep TP larger than SL, and use the same values as your bot if you run one.
Points Per Pip
10
10 suits gold, indices, BTC and 5-decimal FX (1 pip on gold = $0.10).
BUY / SELL colours, HUD Position
Cosmetic
Label colours and where the HUD sits. No effect on signals.
Timezone
Europe/Dublin
Your own timezone, so the market-closed status is right.
Bar Replay Mode
Off
Turn on while using Bar Replay so the HUD keeps updating.
Targets lock in and stay put. The moment a signal fires, entry, TP and SL are fixed and shown in the HUD — they won't shift if the market moves. Put those exact numbers into your broker.
GuidanceSetting Your Targets▼

TP and SL are set in pips. A pip is tiny on most markets, so the numbers look big — here's what they mean in price.

Market
1 pip =
XAUUSD (gold)
$0.10 — 100 pips = $10
NAS100 / SP500
0.1 point — 100 pips = 10 points
BTCUSD
$0.10 — 1000 pips = $100
5-decimal FX (e.g. AUDUSD)
0.0001 — the usual FX pip
Starting points that tested well (15m): XAUUSD SL 165 / TP 495, SP500 SL 58 / TP 116, NAS100 SL 550 / TP 1100. Full details are on the Auto-Trade page under Recommended Settings.
CriticalWait for Candle Close▼

A BUY or SELL label showing up on your chart isn't the moment to act — the candle it appeared on has to fully close first.

Labels on the still-forming candle can vanish just as fast as they appeared. Open Legacy checks for a trailing-stop crossover on every price tick, including the current, unfinished candle. If price crosses one way and then reverses before that candle closes, the label disappears — that's a fakeout, not a real signal. Enter on it and you're trading a setup that may not even exist a few seconds later.

See a label appear? Don't act yet

Note it, but treat it as provisional — it's still live and can be erased by the next tick.

Wait for that candle to close

Once the candle the label appeared on finishes and the next candle opens, the signal is locked in — no more repainting.

Still there after close? Now it's solidified

That's the moment to consider entering — a confirmed signal, not a mid-candle flicker.

GuidanceTrade Small, Trade Consistently▼

Size every trade from the stop loss, not from how good the setup looks.

Work out what one stop loss costs at your lot size before you take the trade — e.g. a $16.50 stop on gold at 0.10 lots (10 oz) is about $165. Keep that small and steady, and let consistent trades compound rather than swinging big on one.
GuidanceReading the TP Hit Odds▼

The HUD's "TP Hit Odds" figure is a heuristic, not a guarantee — but it's still useful as a filter for when to stay cautious. A simple rule of thumb:

Below 60% before entry

If a signal is active but the HUD's TP Hit Odds reads below 60%, treat that as a caution flag. Consider holding off on entering that trade rather than taking every signal automatically.

40–50% while in a trade

If you're already in a trade and the odds drift down into the 40–50% range or lower, consider closing the position early rather than waiting for SL to be hit.

These are thresholds for caution, not rules for certainty. The odds figure is built from price distance to target and correlation with the auto-detected benchmark — it can shift quickly on volatile candles or when that benchmark flips. Use it as one input among your own risk management, not a substitute for it. All trading carries risk of loss regardless of what the HUD reads.
AdvancedThe Correlation Confirmation Secret▼

A second, independent read on strength — checking your instrument against the benchmark that naturally moves opposite it — is what separates a confirmed trade from a rushed one. Open Legacy figures out the right benchmark for whatever you're charting automatically, and surfaces it right in its own HUD table — no second chart, no second script.

Buddha heart · demon hands — reading both sides
BUDDHA HEART Before the trade — Ready DEMON HANDS Once live — Confirmed / Invalid ANY MARKET · READ BOTH SIDES BEFORE YOU MOVE

Nothing to add — it's already built in

Open Legacy's HUD table includes a "[Benchmark] Structure" row automatically, right on whichever chart you put it on. No second chart, no separate script — and no manual symbol picking, unless you want to override it yourself.

Before a trade — the Buddha heart

While you're waiting for a signal, the Structure row simply reads Ready ⏳ — calm, steady, no verdict yet. Nothing to act on, just watching.

Once a trade is live — the demon hands

The instant a BUY or SELL becomes active, that same row flips decisively: Confirmed ✅ means correlation backs your trade — your green light. Invalid ❌ means the trend is running against the expected correlation — a caution flag. You may also see CLOSED 💤 — that shows up when the underlying market is shut, or (for crypto) during the scheduled window when forex markets — and DXY along with them — are offline. Open Legacy bypasses the correlation check automatically during that window rather than penalizing a trade for a benchmark that isn't trading.

Only treat a signal as fully confirmed on Confirmed ✅

When Open Legacy fires a BUY or SELL, check the Structure row before acting. Invalid doesn't mean don't trade — it means treat the signal with extra caution.

How the benchmark gets picked

What you're charting
Auto-matched benchmark
Why
USD pairs & metals (EURUSD, XAUUSD, BTCUSD...)
DXY
The Dollar Index is the standard inverse read against anything priced in USD.
Major indices (S&P 500, NAS100, DAX, UK100...)
VIX
The volatility index tends to spike when equities sell off — a natural inverse gauge.
Indian equities (NSE, BSE)
India VIX
Same volatility-index logic, using the regional benchmark instead.
USDJPY / USDCAD / USDCHF
Yen / CAD / CHF Index
For USD-base pairs, the quote currency's own strength index is the more precise read.
Cross pairs quoted in JPY, GBP, or EUR
Yen / Pound / Euro Index
Matches the benchmark to whichever currency the pair is actually quoted in.
No separate alerts for this one. The Structure row is meant to be read visually in the HUD table — it's not part of the alert message. The single alert covered in Setting Up Alerts below already tells you exactly when to go look.

"To flow in the markets you must have a Buddha heart and demon hands. The Buddha heart is the calm, steady wait for what plausibly is right. The demon hands are the decisive verdict the moment a trade goes live, stating where we are planning and already operating our next move — bullish or bearish."

Calm patience, decisive execution — the same row tells you both.

Session Timing · XAUUSD exampleBest Times to Trade — a Gold Example▼

Signals mean more when they fire during hours where your market typically sees real liquidity and follow-through. Since Open Legacy now works across markets, exact windows vary by instrument — here's a concrete example using XAUUSD's two prime windows, both in UTC+1. Trading something else? Look up your own market's peak-liquidity hours; the same avoid-the-dead-zones principle applies everywhere.

XAUUSD session timeline — UTC+1
XAUUSD · PRIME TRADING WINDOWS (UTC+1) 6 7 8 9 10 11 12 13 14 15 16 17 18 07:00 – 11:30 13:00 – 16:00 12:00–13:00 Prime window — highest liquidity Midday lull — avoid (next section)

07:00 – 11:30 UTC+1

The morning window. London liquidity is online and price action tends to be cleaner and more directional.

13:00 – 16:00 UTC+1

The afternoon window. London and New York overlap here, which is typically where gold sees its strongest follow-through.

Stay inside these windows. Signals firing outside them are more likely to be low-liquidity noise, even if Open Legacy's HUD reads confidently. Treat the two windows above as default trading hours if you're on XAUUSD — for other markets, swap in that market's own high-liquidity hours instead.
Session Timing · XAUUSD exampleAvoid the Midday Lull▼

Between the two prime XAUUSD windows above sits a gap worth respecting on purpose. (Other markets have their own version of this dead zone — check yours.)

12:00 – 13:00 UTC+1 — the midday lull, also known as "coffee hour." Volume thins out as the London morning session winds down and New York hasn't opened yet. Price action here tends to chop sideways rather than trend, which can produce signals that look valid but fade quickly. Sit this hour out and wait for the 13:00 UTC+1 window instead.
Risk GuidanceNews Awareness▼

Every market is sensitive to its own macro surprises. Open Legacy isn't designed to trade through that kind of volatility, whatever you're charting.

Avoid trading around medium or high impact news events. Check an economic calendar for scheduled releases relevant to your market — CPI, NFP, and Fed decisions for USD pairs and gold, ECB decisions for EUR pairs, earnings and central bank guidance for indices, and so on — and stay out of the market from 30 minutes before to 30 minutes after any medium or high impact release, not just during the release itself. Spreads widen and price can spike through both TP and SL levels in seconds, which the HUD's odds engine isn't built to anticipate.
03Setting Up Alerts▼

Alerts are configured on Open Legacy with a single alert — one per pair. That one alert covers all four events — new buy trade, new sell trade, TP hit, and SL hit — automatically, so there's nothing to split into multiple alerts. Trading more than one pair? Each one needs its own chart and its own alert.

Right-click the chart and choose "Add Alert"

Or click the alarm clock icon in the right-hand toolbar.

Set the condition to Open Legacy

In the "Condition" dropdown, select the Open Legacy indicator — it's the only one on your chart now.

Choose "Any alert() function call"

Open Legacy now fires everything through one built-in alert() call, so this is the only option you need — no more picking between separate conditions.

Set your preferred interval

Pick whichever timeframe matches what you're trading. If you're not sure, 10 minutes is a solid default — frequent enough to catch signals early without flooding you with noise.

Name it and customize the message if you like

Give the alert any name you want — e.g. "Open Legacy Alert". The actual notification content (entry, TP, SL, or a hit confirmation) is generated automatically by the script, so the message field is just for your own reference.

Connect a notification channel

Turn on app push, email, SMS, or webhook delivery in the "Notifications" tab of the alert dialog, then click Create.

One alert does it all. Open Legacy's alert() call sends a different, detailed message depending on what just happened — new buy trade, new sell trade, TP hit, or SL hit — all four covered by the one alert you set up. Nothing else to configure.

All four events, one alert

BUY

New Buy Trade — a new BUY becomes active, with entry, TP, and SL included.

"🚀 {{ticker}} New BUY Active! 📌 Entry: ... 🎯 TP: ... 🛡️ SL: ..."

SELL

New Sell Trade — a new SELL becomes active, with entry, TP, and SL included.

"🚀 {{ticker}} New SELL Active! 📌 Entry: ... 🎯 TP: ... 🛡️ SL: ..."

TP

Take Profit Hit — price reaches the locked TP level.

"🎯 {{ticker}} Take Profit Hit! ✨ Reached Target (+X pts)"

SL

Stop Loss Hit — price reaches the locked SL level.

"🛑 {{ticker}} Stop Loss Hit! ⚠️ Breached (-X pts)"

Copy & pasteGet the Scripts▼

Full Pine Script v6 source for both. Copy each into its own Pine Editor tab as described in How to Install.

Open Legacy Any market
//@version=6
indicator("Open Legacy", overlay = true)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// OPEN LEGACY — manual signals with a live HUD.
// The same two strategies and Sensitivity dial as Open Legacy Bot, so the
// same settings give the same BUY / SELL signals.
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// ── 1. STRATEGY ──────────────────────────────────────────────────────────
G1 = "1. Strategy"
entryMethod = input.string("FVG Pullback", "Entry Method", options = ["FVG Pullback", "EMA Trend"], group = G1,
     tooltip = "FVG Pullback — price leaves a fair value gap (a candle so fast it leaves an untouched gap), then a signal fires when price pulls back into the gap and turns.\n\nEMA Trend — signals at the start of a new trend leg: fast EMA above slow EMA, price above the fast EMA and RSI above 50 (the reverse for sells).")
sensitivity = input.string("Balanced", "Sensitivity", options = ["Aggressive", "Balanced", "Conservative"], group = G1,
     tooltip = "Aggressive — the most signals, any timeframe.\n\nBalanced — only signals that agree with the 50 EMA trend.\n\nConservative — also needs the trend on a timeframe 4x your chart to agree, and real trend strength (ADX 20+). The fewest signals, the highest quality.")

// ── 2. TARGETS ───────────────────────────────────────────────────────────
G2 = "2. Targets"
slPips    = input.float(100, "Stop Loss (pips)", minval = 0.1, step = 0.1, group = G2)
tpPips    = input.float(200, "Take Profit (pips)", minval = 0.1, step = 0.1, group = G2,
     tooltip = "Keep Take Profit larger than Stop Loss. Use the same values as your bot so both show the same levels.")
pipPoints = input.int(10, "Points Per Pip", minval = 1, group = G2,
     tooltip = "10 for gold, indices, BTC and 5-decimal FX.")

// ── 3. DISPLAY ───────────────────────────────────────────────────────────
G3 = "3. Display"
buyColor   = input.color(#4CAF50, "BUY", group = G3, inline = "c")
sellColor  = input.color(#FF5252, "SELL", group = G3, inline = "c")
hudPos     = input.string("Top Right", "HUD Position", options = ["Top Right", "Top Left", "Bottom Right", "Bottom Left"], group = G3)
sessionTz  = input.string("Europe/Dublin", "Timezone", group = G3,
     tooltip = "Your timezone, IANA format — used to show when the market is closed.")
replayMode = input.bool(false, "Bar Replay Mode", group = G3,
     tooltip = "Turn on while using Bar Replay so the HUD keeps updating.")

//── Internal constants ──
useExchangeTz = false
useCorrFilter = true
buyBgColor    = buyColor
buyTextColor  = color.white
sellBgColor   = sellColor
sellTextColor = color.white
corrEmaLen    = 9
corrConfBars  = 3
gracePeriodBars = 6
maxRampBars     = 12

getTablePosition(string pos) =>
    switch pos
        "Top Left"     => position.top_left
        "Top Right"    => position.top_right
        "Bottom Left"  => position.bottom_left
        "Bottom Right" => position.bottom_right
        => position.bottom_right

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🌐 UNIVERSAL HANDS-FREE BENCHMARK ROUTER
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Returns [benchmarkSymbol, isInverseRelationship]
// isInverseRelationship = true  -> benchmark moving UP pushes asset toward SL if asset is a BUY
//                                  (i.e. benchmark should move OPPOSITE the asset to favor TP)
// isInverseRelationship = false -> benchmark should move WITH the asset to favor TP
getUniversalInverseBenchmark() =>
    string p     = syminfo.prefix
    string t     = str.upper(syminfo.ticker)
    string base  = syminfo.basecurrency
    string quote = syminfo.currency

    string symbol  = "TVC:DXY"
    bool   inverse = true

    if t == "USDX" or t == "DXY" or t == "USDOLLAR" or t == "DX"
        symbol := "TVC:GOLD"
        inverse := true
    else if p == "NSE" or p == "BSE" or quote == "INR"
        symbol := "NSE:INDIAVIX"
        inverse := true
    else if str.contains(t, "SPX") or str.contains(t, "US500") or str.contains(t, "SP500") or str.contains(t, "NDX") or str.contains(t, "US100") or str.contains(t, "NAS100") or str.contains(t, "US30") or str.contains(t, "DJI") or str.contains(t, "GER") or str.contains(t, "DAX") or str.contains(t, "UK100") or str.contains(t, "EU50") or p == "NASDAQ" or p == "NYSE"
        symbol := "TVC:VIX"
        inverse := true
    else if base == "USD"
        if quote == "JPY"
            symbol := "TVC:JXY"
            inverse := true
        else if quote == "CAD"
            symbol := "TVC:CXY"
            inverse := true
        else if quote == "CHF"
            symbol := "TVC:SXY"
            inverse := true
        else
            // USD is the BASE here (USDSEK, USDMXN, USDZAR, USDTRY, USDHKD, etc.)
            // DXY moves WITH these pairs, not against them — DIRECT correlation
            symbol := "TVC:DXY"
            inverse := false
    else if quote == "USD"
        symbol := "TVC:DXY"
        inverse := true
    else if quote == "JPY"
        symbol := "TVC:JXY"
        inverse := true
    else if quote == "GBP"
        symbol := "TVC:BXY"
        inverse := true
    else if quote == "EUR"
        symbol := "TVC:EXY"
        inverse := true

    [symbol, inverse]

getBenchmarkDisplayName(string symbol) =>
    switch symbol
        "NSE:INDIAVIX" => "India VIX"
        "TVC:VIX"      => "VIX"
        "TVC:DXY"      => "DXY"
        "TVC:GOLD"     => "Gold"
        "TVC:JXY"      => "Yen Index"
        "TVC:BXY"      => "Pound Index"
        "TVC:EXY"      => "Euro Index"
        "TVC:CXY"      => "CAD Index"
        "TVC:SXY"      => "CHF Index"
        => symbol

[routedBenchSymbol, routedBenchInverse] = getUniversalInverseBenchmark()

string activeBenchSymbol  = routedBenchSymbol
bool   activeBenchInverse = routedBenchInverse
string activeBenchName    = getBenchmarkDisplayName(activeBenchSymbol)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🌙 MARKET-CLOSE DETECTOR — scheduled weekly close OR unscheduled outage
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
bool isCrypto = syminfo.type == "crypto" or str.contains(syminfo.ticker, "BTC") or str.contains(syminfo.ticker, "ETH")

// Scheduled weekly close, evaluated against the REAL current clock (timenow) in
// the chosen timezone. DST is handled automatically via IANA tz names.
// • Mon-Fri: closed 22:00-22:59 local (daily break), reopens 23:00
// • Fri 22:00 onward through all of Sat through Sun 22:59: weekend closed
// • Reopens Sunday 23:00 local
f_isScheduledClose(string tz) =>
    int dow = dayofweek(timenow, tz)  // Pine: 1 = Sunday ... 7 = Saturday
    int hr  = hour(timenow, tz)
    bool weekdayBreak  = (dow >= 2 and dow <= 6) and (hr == 22)
    bool weekendClosed = (dow == 6 and hr >= 22) or (dow == 7) or (dow == 1 and hr < 23)
    weekdayBreak or weekendClosed

bool scheduledClosedNow = f_isScheduledClose(useExchangeTz ? syminfo.timezone : sessionTz)

// Live "no fresh data" detector — catches an UNSCHEDULED outage or maintenance
// break of any length (not a fixed window): as long as no new bar has printed
// for 15+ minutes on a genuinely live last bar, it stays flagged closed for
// however long that gap continues, and clears the moment fresh data resumes.
// Only ever evaluated on barstate.isrealtime, so historical/replay bars never
// trigger this regardless of how old their timestamps are.
bool dataStaleNow = barstate.isrealtime and barstate.islast and (timenow - time_close > 900000)

// DXY/forex benchmark closes on the standard schedule even when crypto itself
// keeps trading — bypass the correlation filter during that window.
bool isCryptoDxyOffline = not replayMode and isCrypto and scheduledClosedNow

// Main HUD "market closed" status:
// • non-crypto: closed if EITHER the scheduled weekly window applies OR the
//   feed has genuinely gone stale (unscheduled maintenance/outage)
// • crypto: trades 24/7, so the fixed weekly schedule doesn't apply to the
//   asset itself — only genuine exchange downtime (stale feed) marks it closed
bool isMarketClosedRT = not replayMode and (dataStaleNow or (not isCrypto and scheduledClosedNow))

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 📊 SIGNAL ENGINE — identical to Open Legacy Bot
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Sensitivity presets (literal values keep lengths "simple" for ta.* calls)
fvgLife  = sensitivity == "Aggressive" ? 12  : sensitivity == "Balanced" ? 24  : 40
fvgMin   = sensitivity == "Aggressive" ? 0.10 : sensitivity == "Balanced" ? 0.25 : 0.40
emaFastL = sensitivity == "Aggressive" ? 8   : sensitivity == "Balanced" ? 13  : 21
emaSlowL = sensitivity == "Aggressive" ? 21  : sensitivity == "Balanced" ? 34  : 55

atrV  = ta.atr(14)
ema50 = ta.ema(close, 50)
[diP, diM, adxV] = ta.dmi(14, 14)

// Higher-timeframe trend (4x chart), previous completed candle — never repaints.
htfTf  = timeframe.from_seconds(timeframe.in_seconds() * 4)
htfC   = request.security(syminfo.tickerid, htfTf, close[1], lookahead = barmerge.lookahead_on)
htfE   = request.security(syminfo.tickerid, htfTf, ta.ema(close, 50)[1], lookahead = barmerge.lookahead_on)
bool htfUp = not na(htfE) and htfC > htfE
bool htfDn = not na(htfE) and htfC < htfE

// ── FVG Pullback ──
var float bullTop = na
var float bullBot = na
var int   bullBar = na
var float bearTop = na
var float bearBot = na
var int   bearBar = na
bool fvgBuy  = not na(bullBar) and bar_index - bullBar > 0 and bar_index - bullBar <= fvgLife and low <= bullTop and close > open and close > bullBot
bool fvgSell = not na(bearBar) and bar_index - bearBar > 0 and bar_index - bearBar <= fvgLife and high >= bearBot and close < open and close < bearTop
if fvgBuy
    bullBar := na
if fvgSell
    bearBar := na
if low > high[2] and (low - high[2]) >= fvgMin * atrV
    bullBot := high[2]
    bullTop := low
    bullBar := bar_index
if high < low[2] and (low[2] - high) >= fvgMin * atrV
    bearTop := low[2]
    bearBot := high
    bearBar := bar_index

// ── EMA Trend ──
emaF = ta.ema(close, emaFastL)
emaS = ta.ema(close, emaSlowL)
rsiV = ta.rsi(close, 14)
bool trUp = emaF > emaS and close > emaF and rsiV > 50
bool trDn = emaF < emaS and close < emaF and rsiV < 50
bool trendBuy  = trUp and not trUp[1]
bool trendSell = trDn and not trDn[1]

// ── Sensitivity filter ──
bool rawBuy  = entryMethod == "FVG Pullback" ? fvgBuy  : trendBuy
bool rawSell = entryMethod == "FVG Pullback" ? fvgSell : trendSell
bool qBuy  = sensitivity == "Aggressive" or (close > ema50 and (sensitivity == "Balanced" or (htfUp and adxV >= 20)))
bool qSell = sensitivity == "Aggressive" or (close < ema50 and (sensitivity == "Balanced" or (htfDn and adxV >= 20)))
bool sigBuy  = rawBuy and qBuy
bool sigSell = rawSell and qSell and not sigBuy
slowEma = ema50


rawBenchClose = request.security(activeBenchSymbol, timeframe.period, close, barmerge.gaps_off, barmerge.lookahead_off)
rawBenchEma   = request.security(activeBenchSymbol, timeframe.period, ta.ema(close, corrEmaLen), barmerge.gaps_off, barmerge.lookahead_off)

benchClose = nz(rawBenchClose, close)
benchEma   = nz(rawBenchEma, ta.ema(close, corrEmaLen))

bool rawBenchBullish = benchClose > benchEma
bool rawBenchBearish = benchClose < benchEma
bool benchBullish = ta.lowest(rawBenchBullish ? 1 : 0, corrConfBars) == 1
bool benchBearish = ta.lowest(rawBenchBearish ? 1 : 0, corrConfBars) == 1

// benchFavorsBuy/Sell = benchmark is moving in the direction that supports price
// heading toward TP (not SL) for the current trade direction.
bool benchFavorsBuy  = activeBenchInverse ? benchBearish : benchBullish
bool benchFavorsSell = activeBenchInverse ? benchBullish : benchBearish

bool corrBuyConfirm  = not useCorrFilter or isCryptoDxyOffline or benchFavorsBuy
bool corrSellConfirm = not useCorrFilter or isCryptoDxyOffline or benchFavorsSell


// ── Signals ──
bool confirmedBar = barstate.isconfirmed or replayMode
bool buy1  = sigBuy
bool sell1 = sigSell
newBuySignal  = buy1
newSellSignal = sell1

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🔒 MULTI-TIER PERSISTENT FIXED RISK ENGINE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

var bool   tradeActive     = false
var string activeSignalTag = "WAITING"
var float  lockedEntry     = na
var float  lockedTpPrice   = na
var float  lockedSlPrice   = na
var bool   activeIsBuy     = false
var string lastTpText      = "---"
var string lastSlText      = "---"

var int benchConfirmStreak = 0
var int benchInvalidStreak = 0
var int trendConfirmStreak = 0
var int trendInvalidStreak = 0

var line  lineSL  = na
var line  lineTP  = na
var label labelSL = na
var label labelTP = na
var int   tradeEntryBar = na

string masterAlertMsg = ""

if (newBuySignal or newSellSignal) and (barstate.isconfirmed or replayMode)
    activeIsBuy := newBuySignal

    finalTpDist = tpPips * pipPoints * syminfo.mintick
    finalSlDist = slPips * pipPoints * syminfo.mintick

    lockedEntry   := close
    lockedTpPrice := activeIsBuy ? close + finalTpDist : close - finalTpDist
    lockedSlPrice := activeIsBuy ? close - finalSlDist : close + finalSlDist
    tradeEntryBar := bar_index

    string tag = newBuySignal ? "BUY" : "SELL"
    activeSignalTag := tag + " ACTIVE"
    tradeActive     := true

    lastTpText      := "---"
    lastSlText      := "---"

    benchConfirmStreak := 0
    benchInvalidStreak := 0
    trendConfirmStreak := 0
    trendInvalidStreak := 0

    masterAlertMsg  := "🚀 " + syminfo.ticker + " (" + timeframe.period + ") New " + tag + " Active!" + "\n📌 Entry: " + str.tostring(lockedEntry, "#.##") + "\n🎯 TP: " + str.tostring(lockedTpPrice, "#.##") + "\n🛡️ SL: " + str.tostring(lockedSlPrice, "#.##")

    if not na(lineSL)
        line.delete(lineSL)
    if not na(lineTP)
        line.delete(lineTP)
    if not na(labelSL)
        label.delete(labelSL)
    if not na(labelTP)
        label.delete(labelTP)

    int endX = bar_index + 12
    lineSL  := line.new(bar_index, lockedSlPrice, endX, lockedSlPrice, color = color.red, width = 2, style = line.style_dashed)
    labelSL := label.new(endX, lockedSlPrice, tag + " SL: " + str.tostring(lockedSlPrice, "#.##"), style = label.style_label_left, color = color.red, textcolor = color.white, size = size.tiny)

    lineTP  := line.new(bar_index, lockedTpPrice, endX, lockedTpPrice, color = color.green, width = 2, style = line.style_dashed)
    labelTP := label.new(endX, lockedTpPrice, tag + " TP: " + str.tostring(lockedTpPrice, "#.##"), style = label.style_label_left, color = color.green, textcolor = color.white, size = size.tiny)

if tradeActive and not na(lockedTpPrice) and not na(lockedSlPrice) and bar_index > tradeEntryBar
    bool tpHit = activeIsBuy ? (high >= lockedTpPrice) : (low <= lockedTpPrice)
    bool slHit = activeIsBuy ? (low <= lockedSlPrice)  : (high >= lockedSlPrice)

    if tpHit or slHit
        if tpHit
            float tpDistance = math.abs(lockedTpPrice - lockedEntry)
            activeSignalTag := "TP HIT 🎉"
            lastTpText      := "Hit (+ " + str.tostring(tpDistance, "#.##") + ")"
            masterAlertMsg  := "🎯 " + syminfo.ticker + " (" + timeframe.period + ") Take Profit Hit!\n✨ Reached Target (+ " + str.tostring(tpDistance, "#.##") + " pts)"
        else if slHit
            float slDistance = math.abs(lockedSlPrice - lockedEntry)
            activeSignalTag := "SL HIT ❌"
            lastSlText      := "Hit (- " + str.tostring(slDistance, "#.##") + ")"
            masterAlertMsg  := "🛑 " + syminfo.ticker + " (" + timeframe.period + ") Stop Loss Hit!\n⚠️ Breached (- " + str.tostring(slDistance, "#.##") + " pts)"

        tradeActive := false
        if not na(lineSL)
            line.delete(lineSL)
            lineSL := na
        if not na(lineTP)
            line.delete(lineTP)
            lineTP := na
        if not na(labelSL)
            label.delete(labelSL)
            labelSL := na
        if not na(labelTP)
            label.delete(labelTP)
            labelTP := na

if masterAlertMsg != ""
    alert(masterAlertMsg, alert.freq_once_per_bar)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🧠 UPGRADED PROBABILITY & BENCHMARK SCORING ENGINE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

bool isCorrelationConfirmedNow = (activeIsBuy and benchFavorsBuy) or (not activeIsBuy and benchFavorsSell)
bool isTrendAlignedNow         = (activeIsBuy and close > slowEma) or (not activeIsBuy and close < slowEma)

if tradeActive
    if isCorrelationConfirmedNow
        benchConfirmStreak := benchConfirmStreak + 1
        benchInvalidStreak := 0
    else
        benchInvalidStreak := benchInvalidStreak + 1
        benchConfirmStreak := 0

    if isTrendAlignedNow
        trendConfirmStreak := trendConfirmStreak + 1
        trendInvalidStreak := 0
    else
        trendInvalidStreak := trendInvalidStreak + 1
        trendConfirmStreak := 0

f_persistBonus(confirmStreak, invalidStreak, maxBonus, grace, rampBars) =>
    float bonus = 0.0
    if confirmStreak > 0
        float rampProgress = math.min(confirmStreak, rampBars) / rampBars
        bonus := (maxBonus * 0.5) + (maxBonus * 0.5) * rampProgress
    else if invalidStreak > grace
        float decayBars = invalidStreak - grace
        bonus := -math.min(maxBonus, maxBonus * (decayBars / rampBars))
    bonus

float rawBenchBonus = isCryptoDxyOffline ? 0.0 : f_persistBonus(benchConfirmStreak, benchInvalidStreak, 10.0, gracePeriodBars, maxRampBars)
float rawTrendBonus = f_persistBonus(trendConfirmStreak, trendInvalidStreak, 6.0, gracePeriodBars, maxRampBars)

float smoothTrendBonus = ta.sma(rawTrendBonus, 5)
float smoothBenchBonus = ta.sma(rawBenchBonus, 5)

float currentProgress = tradeActive and not na(lockedTpPrice) and not na(lockedSlPrice) ? (activeIsBuy ? ((close - lockedSlPrice) / (lockedTpPrice - lockedSlPrice)) : ((lockedSlPrice - close) / (lockedSlPrice - lockedTpPrice))) : 0.0
float smoothedProg    = ta.sma(currentProgress, 2)

float tpOdds = na
if activeSignalTag == "TP HIT 🎉"
    tpOdds := 100.0
else if activeSignalTag == "SL HIT ❌"
    tpOdds := 0.0
else if tradeActive and not na(lockedTpPrice) and not na(lockedSlPrice)
    float distOdds = math.min(100.0, math.max(0.0, smoothedProg * 100.0))
    tpOdds := math.min(99.0, math.max(1.0, (distOdds * 0.80) + 10.0 + smoothTrendBonus + smoothBenchBonus))

// HUD label uses the same streaks as the odds score — only flips to Invalid once
// the benchmark has pushed toward SL for longer than the grace period, and flips
// back to Confirmed as soon as it favors TP again.
bool isCorrelationConfirmedForLabel = benchConfirmStreak > 0 or benchInvalidStreak <= gracePeriodBars

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 📱 DYNAMIC HUD TABLE RENDERER
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

var table hud = table.new(position = getTablePosition(hudPos), columns = 2, rows = 7, bgcolor = color.rgb(18, 18, 24, 10), border_color = color.rgb(40, 40, 50), border_width = 1)

if barstate.islast or replayMode
    color headerBg = color.rgb(25, 30, 45)
    color rowBg1   = color.rgb(20, 20, 28)
    color rowBg2   = color.rgb(14, 14, 20)
    color baseBg   = color.rgb(8, 8, 12)

    bool hasHitTargetOrStop = (activeSignalTag == "TP HIT 🎉" or activeSignalTag == "SL HIT ❌")

    string topHeaderStatus = isMarketClosedRT ? "CLOSED 💤" : (hasHitTargetOrStop ? "Waiting ⏳" : activeSignalTag)
    color topHeaderColor   = isMarketClosedRT ? color.rgb(150, 150, 160) : (hasHitTargetOrStop ? color.rgb(241, 196, 15) : (str.contains(activeSignalTag, "BUY") ? color.rgb(46, 204, 113) : color.rgb(231, 76, 60)))

    string correlStatus = isCryptoDxyOffline ? "CLOSED 💤" : (isMarketClosedRT ? "CLOSED 💤" : (not tradeActive ? "Ready ⏳" : (isCorrelationConfirmedForLabel ? "Confirmed ✅" : "Invalid ❌")))

    table.cell(hud, 0, 0, "Open Legacy (" + syminfo.ticker + ")", text_color=color.rgb(220, 225, 235), bgcolor=headerBg, text_size=size.small)
    table.cell(hud, 1, 0, topHeaderStatus, text_color=topHeaderColor, bgcolor=headerBg, text_size=size.small)

    string statusOddsVal  = isMarketClosedRT ? "Market Closed 💤" : (hasHitTargetOrStop ? "Waiting ⏳" : (activeSignalTag == "WAITING" ? "Waiting..." : str.tostring(tpOdds, "#.##") + "%"))
    string fixedEntryVal  = isMarketClosedRT ? "CLOSED 💤" : (hasHitTargetOrStop ? "Waiting ⏳" : (na(lockedEntry) ? "Waiting..." : str.tostring(lockedEntry, "#.##")))
    string takeProfitVal  = isMarketClosedRT ? "CLOSED 💤" : (hasHitTargetOrStop ? "Waiting ⏳" : (lastTpText != "---" ? lastTpText : (na(lockedTpPrice) ? "Waiting..." : str.tostring(lockedTpPrice, "#.##"))))
    string stopLossVal    = isMarketClosedRT ? "CLOSED 💤" : (hasHitTargetOrStop ? "Waiting ⏳" : (lastSlText != "---" ? lastSlText : (na(lockedSlPrice) ? "Waiting..." : str.tostring(lockedSlPrice, "#.##"))))
    string benchStructVal = correlStatus

    table.cell(hud, 0, 1, "🎯 Setup Score", text_color=color.rgb(180, 185, 195), bgcolor=rowBg1, text_size=size.small)
    table.cell(hud, 1, 1, statusOddsVal, text_color=isMarketClosedRT ? color.rgb(150, 150, 160) : (hasHitTargetOrStop ? color.rgb(241, 196, 15) : color.rgb(241, 196, 15)), bgcolor=rowBg1, text_size=size.small)

    table.cell(hud, 0, 2, "📌 Fixed Entry", text_color=color.rgb(180, 185, 195), bgcolor=rowBg2, text_size=size.small)
    table.cell(hud, 1, 2, fixedEntryVal, text_color=isMarketClosedRT ? color.rgb(150, 150, 160) : color.white, bgcolor=rowBg2, text_size=size.small)

    table.cell(hud, 0, 3, "🚀 Take Profit", text_color=color.rgb(180, 185, 195), bgcolor=rowBg1, text_size=size.small)
    table.cell(hud, 1, 3, takeProfitVal, text_color=isMarketClosedRT ? color.rgb(150, 150, 160) : color.rgb(46, 204, 113), bgcolor=rowBg1, text_size=size.small)

    table.cell(hud, 0, 4, "🛡️ Stop Loss", text_color=color.rgb(180, 185, 195), bgcolor=rowBg2, text_size=size.small)
    table.cell(hud, 1, 4, stopLossVal, text_color=isMarketClosedRT ? color.rgb(150, 150, 160) : color.rgb(231, 76, 60), bgcolor=rowBg2, text_size=size.small)

    table.cell(hud, 0, 5, "🌐 " + activeBenchName + " Structure", text_color=color.rgb(200, 210, 230), bgcolor=rowBg1, text_size=size.small)
    table.cell(hud, 1, 5, benchStructVal, text_color=isCryptoDxyOffline ? color.rgb(150, 150, 160) : (isMarketClosedRT ? color.rgb(150, 150, 160) : (correlStatus == "Ready ⏳" ? color.rgb(150, 150, 160) : (isCorrelationConfirmedForLabel ? color.rgb(46, 204, 113) : color.rgb(231, 76, 60)))), bgcolor=rowBg1, text_size=size.small)

    bool structTrendUp    = close > slowEma
    bool structTrendDown  = close < slowEma
    bool structCorrBull   = activeBenchInverse ? benchBearish : benchBullish
    bool structCorrBear   = activeBenchInverse ? benchBullish : benchBearish
    bool structAgree      = (structTrendUp and structCorrBull) or (structTrendDown and structCorrBear)
    bool structConflict   = (structTrendUp and structCorrBear) or (structTrendDown and structCorrBull)
    string structureLabel = not tradeActive ? "Waiting ⏳" : (structAgree ? "Strong" : (structConflict ? "Weak" : "Decent"))
    color  structureColor = not tradeActive ? color.rgb(150, 150, 160) : (structAgree ? color.rgb(46, 204, 113) : (structConflict ? color.rgb(231, 76, 60) : color.rgb(241, 196, 15)))
    table.cell(hud, 0, 6, "Overall Structure", text_color=color.rgb(180, 185, 195), bgcolor=baseBg, text_size=size.small)
    table.cell(hud, 1, 6, structureLabel, text_color=structureColor, bgcolor=baseBg, text_size=size.small)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 🎨 SIGNAL LABELS — only on a closed candle, so a label never vanishes.
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
plotshape(buy1  and confirmedBar, title = "BUY Label",  style = shape.labelup,   location = location.belowbar, color = buyBgColor,  text = "BUY",  textcolor = buyTextColor,  size = size.small)
plotshape(sell1 and confirmedBar, title = "SELL Label", style = shape.labeldown, location = location.abovebar, color = sellBgColor, text = "SELL", textcolor = sellTextColor, size = size.small)
Open Legacy Scoreboard Weekly TP / SL tally
//@version=6
indicator("Open Legacy Scoreboard", overlay = true, max_labels_count = 500)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// OPEN LEGACY SCOREBOARD — for manual traders.
// Replays the exact signals of Open Legacy / Open Legacy Bot with your
// sessions, blackouts and pip TP/SL, and tallies how each trade ended:
// TP hit, SL hit, or closed early in profit or loss. Use the same settings
// as your indicator or bot. (Bot users don't need this — the bot has the
// scoreboard built in.)
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// ── 1. STRATEGY ───────────────────────────────────────────────────────────
G1 = "1. Strategy"
entryMethod = input.string("FVG Pullback", "Entry Method", options = ["FVG Pullback", "EMA Trend"], group = G1,
     tooltip = "FVG Pullback — price leaves a fair value gap (a candle so fast it leaves an untouched gap), then the bot enters when price pulls back into the gap and turns.\n\nEMA Trend — enters at the start of a new trend leg: fast EMA above slow EMA, price above the fast EMA and RSI above 50 (the reverse for sells).")
sensitivity = input.string("Balanced", "Sensitivity", options = ["Aggressive", "Balanced", "Conservative"], group = G1,
     tooltip = "Aggressive — the most trades. Every valid setup, works on any timeframe.\n\nBalanced — only setups that agree with the 50 EMA trend. Roughly half the trades of Aggressive.\n\nConservative — also needs the trend on a timeframe 4x your chart to agree, and real trend strength (ADX 20+). The fewest trades, the highest quality.")
tfLock = input.string("Off", "Only Trade On Timeframe", options = ["Off", "1", "3", "5", "15", "30", "60"], group = G1,
     tooltip = "Off — trades on whatever timeframe the chart (and alert) uses. Pick a value to stop it trading on any other timeframe by mistake.")

// ── 2. SESSIONS ───────────────────────────────────────────────────────────
G2 = "2. Sessions"
s1Time = input.session("0700-1200", "Session 1", group = G2, inline = "s1")
s1Days = input.string("Mon-Fri", "", options = ["Mon-Fri", "Tue-Fri", "Mon-Thu", "Tue-Thu", "Mon-Wed", "Wed-Fri", "Sun-Fri", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat-Sun", "Sat", "Sun", "Every Day", "Off"], group = G2, inline = "s1",
     tooltip = "📆 Pick the days each session trades. Off switches the session off.")
s2Time = input.session("1400-1800", "Session 2", group = G2, inline = "s2")
s2Days = input.string("Off", "", options = ["Mon-Fri", "Tue-Fri", "Mon-Thu", "Tue-Thu", "Mon-Wed", "Wed-Fri", "Sun-Fri", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat-Sun", "Sat", "Sun", "Every Day", "Off"], group = G2, inline = "s2")
sessionTz = input.string("Europe/Dublin", "Timezone", group = G2,
     tooltip = "Your timezone, IANA format — e.g. Europe/Dublin, Europe/London, America/New_York.")
closeAtSessionEnd = input.bool(true, "Close Trades At Session End", group = G2)

// ── 3. NEWS BLACKOUTS ─────────────────────────────────────────────────────
G3 = "3. News Blackouts"
newsSide = input.int(30, "Minutes Either Side", minval = 1, group = G3,
     tooltip = "No new trades from this many minutes before each event until this many minutes after. Any open trade is closed as the window starts.")
n1On   = input.bool(false, "News 1", group = G3, inline = "n1")
n1Date = input.time(timestamp("2026-10-13T12:00:00"), "", group = G3, inline = "n1")
n1Hour = input.int(13, "at", minval = 0, maxval = 23, group = G3, inline = "n1")
n1Min  = input.int(30, ":", minval = 0, maxval = 59, group = G3, inline = "n1")
n2On   = input.bool(false, "News 2", group = G3, inline = "n2")
n2Date = input.time(timestamp("2026-10-14T12:00:00"), "", group = G3, inline = "n2")
n2Hour = input.int(13, "at", minval = 0, maxval = 23, group = G3, inline = "n2")
n2Min  = input.int(30, ":", minval = 0, maxval = 59, group = G3, inline = "n2")
n3On   = input.bool(false, "News 3", group = G3, inline = "n3")
n3Date = input.time(timestamp("2026-10-15T12:00:00"), "", group = G3, inline = "n3")
n3Hour = input.int(13, "at", minval = 0, maxval = 23, group = G3, inline = "n3")
n3Min  = input.int(30, ":", minval = 0, maxval = 59, group = G3, inline = "n3")

// ── 4. TARGETS ───────────────────────────────────────────────────────────
G4 = "4. Targets"
slPips    = input.float(100, "Stop Loss (pips)", minval = 0.1, step = 0.1, group = G4)
tpPips    = input.float(200, "Take Profit (pips)", minval = 0.1, step = 0.1, group = G4)
pipPoints = input.int(10, "Points Per Pip", minval = 1, group = G4,
     tooltip = "10 for gold, indices, BTC and 5-decimal FX.")

// ── 5. DISPLAY ────────────────────────────────────────────────────────────
G5 = "5. Display"
sbShow      = input.bool(true, "📊 Show Scoreboard", group = G5)
sbPeriod    = input.string("This Week", "📅 Show Results For", options = ["This Week", "Last Week", "2 Weeks Ago", "3 Weeks Ago", "4 Weeks Ago", "Last 2 Weeks", "Last 4 Weeks", "Last 8 Weeks", "Last 12 Weeks", "All Time"], group = G5,
     tooltip = "📅 One week: This Week, Last Week, 2-4 Weeks Ago.\n📈 Several weeks added up: Last 2/4/8/12 Weeks (this week included).\n♾️ All Time: every trade on the chart, best for backtesting.")
showMarks   = input.bool(true, "Show Trade Markers", group = G5,
     tooltip = "Marks each counted entry, and where it settled.")

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ENGINE
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
bool tfOk = tfLock == "Off" or timeframe.period == tfLock
int  tfMin = timeframe.in_seconds() / 60

// Sensitivity presets (literal values keep lengths "simple" for ta.* calls)
fvgLife  = sensitivity == "Aggressive" ? 12  : sensitivity == "Balanced" ? 24  : 40
fvgMin   = sensitivity == "Aggressive" ? 0.10 : sensitivity == "Balanced" ? 0.25 : 0.40
emaFastL = sensitivity == "Aggressive" ? 8   : sensitivity == "Balanced" ? 13  : 21
emaSlowL = sensitivity == "Aggressive" ? 21  : sensitivity == "Balanced" ? 34  : 55

atrV  = ta.atr(14)
ema50 = ta.ema(close, 50)
[diP, diM, adxV] = ta.dmi(14, 14)

// Higher-timeframe trend (4x chart), previous completed candle — never repaints.
htfTf  = timeframe.from_seconds(timeframe.in_seconds() * 4)
htfC   = request.security(syminfo.tickerid, htfTf, close[1], lookahead = barmerge.lookahead_on)
htfE   = request.security(syminfo.tickerid, htfTf, ta.ema(close, 50)[1], lookahead = barmerge.lookahead_on)
bool htfUp = not na(htfE) and htfC > htfE
bool htfDn = not na(htfE) and htfC < htfE

// ── FVG Pullback ──
var float bullTop = na
var float bullBot = na
var int   bullBar = na
var float bearTop = na
var float bearBot = na
var int   bearBar = na
bool fvgBuy  = not na(bullBar) and bar_index - bullBar > 0 and bar_index - bullBar <= fvgLife and low <= bullTop and close > open and close > bullBot
bool fvgSell = not na(bearBar) and bar_index - bearBar > 0 and bar_index - bearBar <= fvgLife and high >= bearBot and close < open and close < bearTop
if fvgBuy
    bullBar := na
if fvgSell
    bearBar := na
if low > high[2] and (low - high[2]) >= fvgMin * atrV
    bullBot := high[2]
    bullTop := low
    bullBar := bar_index
if high < low[2] and (low[2] - high) >= fvgMin * atrV
    bearTop := low[2]
    bearBot := high
    bearBar := bar_index

// ── EMA Trend ──
emaF = ta.ema(close, emaFastL)
emaS = ta.ema(close, emaSlowL)
rsiV = ta.rsi(close, 14)
bool trUp = emaF > emaS and close > emaF and rsiV > 50
bool trDn = emaF < emaS and close < emaF and rsiV < 50
bool trendBuy  = trUp and not trUp[1]
bool trendSell = trDn and not trDn[1]

// ── Sensitivity filter ──
bool rawBuy  = entryMethod == "FVG Pullback" ? fvgBuy  : trendBuy
bool rawSell = entryMethod == "FVG Pullback" ? fvgSell : trendSell
bool qBuy  = sensitivity == "Aggressive" or (close > ema50 and (sensitivity == "Balanced" or (htfUp and adxV >= 20)))
bool qSell = sensitivity == "Aggressive" or (close < ema50 and (sensitivity == "Balanced" or (htfDn and adxV >= 20)))
bool sigBuy  = rawBuy and qBuy
bool sigSell = rawSell and qSell and not sigBuy

// Scoreboard period -> weeks to add up (0 = all) and weeks back
int sbSpan = switch sbPeriod
    "Last 2 Weeks"  => 2
    "Last 4 Weeks"  => 4
    "Last 8 Weeks"  => 8
    "Last 12 Weeks" => 12
    "All Time"      => 0
    => 1
int sbWeeksBack = switch sbPeriod
    "Last Week"   => 1
    "2 Weeks Ago" => 2
    "3 Weeks Ago" => 3
    "4 Weeks Ago" => 4
    => 0

// ── Sessions ──
f_days(string opt) =>
    switch opt
        "Mon-Fri"   => "23456"
        "Tue-Fri"   => "3456"
        "Mon-Thu"   => "2345"
        "Tue-Thu"   => "345"
        "Mon-Wed"   => "234"
        "Wed-Fri"   => "456"
        "Sun-Fri"   => "123456"
        "Mon"       => "2"
        "Tue"       => "3"
        "Wed"       => "4"
        "Thu"       => "5"
        "Fri"       => "6"
        "Sat-Sun"   => "17"
        "Sat"       => "7"
        "Sun"       => "1"
        "Every Day" => "1234567"
        => ""

f_hm(string s, int p) =>
    int(str.tonumber(str.substring(s, p, p + 2)))

f_mins(int t) =>
    hour(t, sessionTz) * 60 + minute(t, sessionTz)

// true when time t falls inside the window on one of its days
f_inSess(int t, string sess, string days) =>
    int st = f_hm(sess, 0) * 60 + f_hm(sess, 2)
    int en = f_hm(sess, 5) * 60 + f_hm(sess, 7)
    int m  = f_mins(t)
    bool dayOk = str.length(days) > 0 and str.contains(days, str.tostring(dayofweek(t, sessionTz)))
    dayOk and (en > st ? (m >= st and m < en) : (m >= st or m < en))

// entries need the candle to CLOSE inside the window with a candle of room left
f_room(string sess) =>
    int en = f_hm(sess, 5) * 60 + f_hm(sess, 7)
    int st = f_hm(sess, 0) * 60 + f_hm(sess, 2)
    int m  = f_mins(time_close)
    int left = en > m ? en - m : 1440 - m + en
    bool inside = en > st ? (m > st and m < en) : (m > st or m < en)
    not closeAtSessionEnd or (inside and left > tfMin)

string d1 = f_days(s1Days)
string d2 = f_days(s2Days)
bool inWin1 = f_inSess(time, s1Time, d1)
bool inWin2 = f_inSess(time, s2Time, d2)
bool inSession = inWin1 or inWin2
bool sessionStarted = inSession and not inSession[1]
bool sessionEnded   = not inSession and inSession[1]
// true on the LAST candle of a session (its close lands on the session end),
// so trades close exactly at the end time, not one candle later.
bool sessionEndsNow = (inWin1 and not f_inSess(time_close, s1Time, d1)) or (inWin2 and not f_inSess(time_close, s2Time, d2))
bool entryWindow = (inWin1 and f_room(s1Time)) or (inWin2 and f_room(s2Time))

// ── News blackouts ──
f_newsTs(int pick, int hh, int mm) =>
    timestamp(sessionTz, year(pick, sessionTz), month(pick, sessionTz), dayofmonth(pick, sessionTz), hh, mm)
int sideMs = newsSide * 60 * 1000
f_inNews(int t, bool on, int ts) =>
    on and t >= ts - sideMs and t < ts + sideMs
bool inBlackout = f_inNews(time_close, n1On, f_newsTs(n1Date, n1Hour, n1Min)) or f_inNews(time_close, n2On, f_newsTs(n2Date, n2Hour, n2Min)) or f_inNews(time_close, n3On, f_newsTs(n3Date, n3Hour, n3Min))
bool blackoutStarted = inBlackout and not inBlackout[1]

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TRADE REPLAY — same rules as the bot: one position at a time, an opposite
// signal reverses it, session end and blackouts close it, TP/SL in pips.
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
float tpDist = tpPips * pipPoints * syminfo.mintick
float slDist = slPips * pipPoints * syminfo.mintick

// 1-minute data decides which level came first when one candle hits both.
[m1H, m1L] = request.security_lower_tf(syminfo.tickerid, "1", [high, low])

// Per week: 14 slots (7 days x 2 sessions) x 4 outcomes
// (0 TP hit, 1 Partial Profit, 2 Partial Loss, 3 SL hit).
var array<int> sbOut = array.new_int(56, 0)
var int sbWeek = 0
f_dayIdx(int t) =>
    int d = dayofweek(t, sessionTz)
    d == dayofweek.sunday ? 6 : d - 2
wkNow = weekofyear(time, sessionTz)
if wkNow != wkNow[1] and bar_index > 0
    sbWeek += 1
    for i = 0 to 55
        array.push(sbOut, 0)

f_record(int et, int kind) =>
    int sIdx = f_inSess(et, s1Time, d1) ? 0 : 1
    int slot = (sbWeek * 14 + f_dayIdx(et) * 2 + sIdx) * 4 + kind
    array.set(sbOut, slot, array.get(sbOut, slot) + 1)

f_mark(bool won, string txt, bool atHigh) =>
    if showMarks
        label.new(bar_index, atHigh ? high : low, txt, style = atHigh ? label.style_label_down : label.style_label_up,
             color = won ? color.new(color.green, 20) : color.new(color.red, 20), textcolor = color.white, size = size.tiny)

var int   posDir = 0
var float posEnt = na
var int   posEt  = na
var int   posBar = na

if barstate.isconfirmed
    // 1. TP / SL on this candle (not the entry candle itself)
    if posDir != 0 and bar_index > posBar
        float tpL = posEnt + posDir * tpDist
        float slL = posEnt - posDir * slDist
        bool tpHit = posDir == 1 ? high >= tpL : low <= tpL
        bool slHit = posDir == 1 ? low <= slL : high >= slL
        if tpHit or slHit
            bool won = tpHit and not slHit
            if tpHit and slHit and array.size(m1H) > 0
                bool done = false
                for m = 0 to array.size(m1H) - 1
                    if not done
                        bool mTp = posDir == 1 ? array.get(m1H, m) >= tpL : array.get(m1L, m) <= tpL
                        bool mSl = posDir == 1 ? array.get(m1L, m) <= slL : array.get(m1H, m) >= slL
                        if mTp or mSl
                            won := mTp and not mSl
                            done := true
            f_record(posEt, won ? 0 : 3)
            f_mark(won, won ? "✓ TP" : "✗ SL", won == (posDir == 1))
            posDir := 0
    // 2. Session end / blackout close
    if posDir != 0 and ((sessionEndsNow and closeAtSessionEnd) or blackoutStarted)
        bool won2 = (close - posEnt) * posDir > 0
        f_record(posEt, won2 ? 1 : 2)
        f_mark(won2, won2 ? "◐ Partial +" : "◐ Partial -", true)
        posDir := 0
    // 3. Entries (an opposite signal closes the open trade first)
    bool canTrade = tfOk and entryWindow and not inBlackout
    int want = canTrade and sigBuy ? 1 : canTrade and sigSell ? -1 : 0
    if want != 0 and want != posDir
        if posDir != 0
            bool won3 = (close - posEnt) * posDir > 0
            f_record(posEt, won3 ? 1 : 2)
            f_mark(won3, won3 ? "◐ Partial +" : "◐ Partial -", true)
        posDir := want
        posEnt := close
        posEt  := time
        posBar := bar_index
        if showMarks
            label.new(bar_index, want == 1 ? low : high, want == 1 ? "BUY" : "SELL", style = want == 1 ? label.style_label_up : label.style_label_down,
                 color = want == 1 ? color.new(color.green, 0) : color.new(color.red, 0), textcolor = color.white, size = size.small)

//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 📊 SCOREBOARD
//━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var table sb = table.new(position.bottom_right, 5, 16, border_width = 1, frame_width = 2,
     frame_color = color.new(#2A3350, 0), border_color = color.new(#1C2334, 0))

// Strategies only run when a candle closes, so on a live chart the newest
// candle isn't "last" until it closes. Drawing on the last historical candle
// too makes the board appear the moment the chart loads.
if (barstate.islastconfirmedhistory or barstate.isrealtime) and sbShow
    bg    = color.new(#0E1320, 0)
    alt   = color.new(#121829, 0)
    head  = color.new(#18203A, 0)
    sub   = color.new(#141B2E, 0)
    green = color.rgb(47, 212, 128)
    red   = color.rgb(255, 93, 108)
    teal  = color.rgb(46, 196, 182)
    amber = color.rgb(245, 185, 66)
    dim   = color.rgb(118, 126, 146)
    ts    = size.small
    bool s2On = d2 != ""

    int lastWk  = sbWeek - sbWeeksBack
    int firstWk = sbSpan == 0 ? 0 : math.max(0, lastWk - sbSpan + 1)
    bool single = sbSpan == 1
    string hdr  = sbSpan == 0 ? "📅 All" : single ? (sbWeeksBack == 0 ? "📅 This Wk" : sbWeeksBack == 1 ? "📅 Last Wk" : "📅 " + str.tostring(sbWeeksBack) + " Wks Ago") : "📅 " + str.tostring(sbSpan) + " Wks"

    // totals per day/session, and per outcome
    array<int> win  = array.new_int(14, 0)
    array<int> loss = array.new_int(14, 0)
    array<int> o1   = array.new_int(4, 0)
    array<int> o2   = array.new_int(4, 0)
    if lastWk >= 0
        for w = firstWk to lastWk
            for cell = 0 to 13
                for k = 0 to 3
                    int v = array.get(sbOut, (w * 14 + cell) * 4 + k)
                    if v > 0
                        if k <= 1
                            array.set(win, cell, array.get(win, cell) + v)
                        else
                            array.set(loss, cell, array.get(loss, cell) + v)
                        if cell % 2 == 0
                            array.set(o1, k, array.get(o1, k) + v)
                        else
                            array.set(o2, k, array.get(o2, k) + v)

    table.cell(sb, 0, 0, hdr, text_color = color.rgb(214, 220, 234), bgcolor = head, text_size = ts)
    table.cell(sb, 1, 0, "✅ S1", text_color = green, bgcolor = head, text_size = ts)
    table.cell(sb, 2, 0, "❌ S1", text_color = red,   bgcolor = head, text_size = ts)
    table.cell(sb, 3, 0, "✅ S2", text_color = green, bgcolor = head, text_size = ts)
    table.cell(sb, 4, 0, "❌ S2", text_color = red,   bgcolor = head, text_size = ts)

    names = array.from("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
    int today = f_dayIdx(time)
    int t1 = 0
    int l1 = 0
    int t2 = 0
    int l2 = 0
    for d = 0 to 6
        bool isToday = d == today and sbWeeksBack == 0 and single
        color rbg = isToday ? color.new(#1A2440, 0) : (d % 2 == 0 ? bg : alt)
        table.cell(sb, 0, d + 1, (isToday ? "▸ " : "") + array.get(names, d),
             text_color = isToday ? color.rgb(77, 232, 255) : color.rgb(205, 212, 228), bgcolor = rbg, text_size = ts)
        for s = 0 to 1
            int wv = array.get(win, d * 2 + s)
            int lv = array.get(loss, d * 2 + s)
            bool off = s == 1 and not s2On
            table.cell(sb, s * 2 + 1, d + 1, off ? "·" : wv == 0 ? "-" : str.tostring(wv), text_color = off or wv == 0 ? dim : green, bgcolor = rbg, text_size = ts)
            table.cell(sb, s * 2 + 2, d + 1, off ? "·" : lv == 0 ? "-" : str.tostring(lv), text_color = off or lv == 0 ? dim : red, bgcolor = rbg, text_size = ts)
            if s == 0
                t1 += wv
                l1 += lv
            else
                t2 += wv
                l2 += lv

    int totW = t1 + t2
    int totN = totW + l1 + l2
    float winPct = totN > 0 ? totW * 100.0 / totN : 0.0
    color totCol = totN == 0 ? dim : winPct > 50 ? green : winPct < 50 ? red : amber
    table.cell(sb, 0, 8, totN == 0 ? (single ? "🏁 Week" : "🏁 Total") : "🏁 " + str.tostring(winPct, "#") + "%", text_color = totCol, bgcolor = head, text_size = ts)
    table.cell(sb, 1, 8, str.tostring(t1), text_color = green, bgcolor = head, text_size = ts)
    table.cell(sb, 2, 8, str.tostring(l1), text_color = red,   bgcolor = head, text_size = ts)
    table.cell(sb, 3, 8, s2On ? str.tostring(t2) : "·", text_color = s2On ? green : dim, bgcolor = head, text_size = ts)
    table.cell(sb, 4, 8, s2On ? str.tostring(l2) : "·", text_color = s2On ? red   : dim, bgcolor = head, text_size = ts)

    // Outcome breakdown
    table.cell(sb, 0, 9, "📊 Outcome", text_color = color.rgb(214, 220, 234), bgcolor = head, text_size = ts, text_halign = text.align_left)
    table.cell(sb, 1, 9, "S1",    text_color = dim, bgcolor = head, text_size = ts)
    table.cell(sb, 2, 9, "S2",    text_color = dim, bgcolor = head, text_size = ts)
    table.cell(sb, 3, 9, "All",   text_color = dim, bgcolor = head, text_size = ts)
    table.cell(sb, 4, 9, "Share", text_color = dim, bgcolor = head, text_size = ts)
    oNames = array.from("🎯 TP Hit", "◐ Partial +", "◐ Partial −", "🛑 SL Hit")
    oCols  = array.from(green, teal, amber, red)
    for k = 0 to 3
        int v1 = array.get(o1, k)
        int v2 = array.get(o2, k)
        int vt = v1 + v2
        float pct = totN > 0 ? vt * 100.0 / totN : 0.0
        int nb = int(math.round(pct / 20.0))
        string meter = ""
        for q = 1 to 5
            meter += q <= nb ? "▰" : "▱"
        color cc  = array.get(oCols, k)
        color rbg = k % 2 == 0 ? bg : alt
        table.cell(sb, 0, 10 + k, array.get(oNames, k), text_color = cc, bgcolor = rbg, text_size = ts, text_halign = text.align_left)
        table.cell(sb, 1, 10 + k, v1 == 0 ? "-" : str.tostring(v1), text_color = v1 == 0 ? dim : cc, bgcolor = rbg, text_size = ts)
        table.cell(sb, 2, 10 + k, not s2On ? "·" : v2 == 0 ? "-" : str.tostring(v2), text_color = not s2On or v2 == 0 ? dim : cc, bgcolor = rbg, text_size = ts)
        table.cell(sb, 3, 10 + k, str.tostring(vt), text_color = vt == 0 ? dim : cc, bgcolor = sub, text_size = ts)
        table.cell(sb, 4, 10 + k, totN == 0 ? "▱▱▱▱▱" : meter + " " + str.tostring(pct, "#") + "%", text_color = totN == 0 ? dim : cc, bgcolor = sub, text_size = ts, text_halign = text.align_left)

    // spacer so the TradingView footer never covers the last row
    for col = 0 to 4
        table.cell(sb, col, 14, " ", bgcolor = color.new(color.black, 100), text_size = ts)

Galaxy Theme Visual only
//@version=6
indicator("Galaxy Theme", overlay=true)

// Create a 10x10 grid. The table background is 100% transparent so your candles remain completely visible!
var table space = table.new(position.top_left, 10, 10, bgcolor=color.new(color.black, 100), border_width=0)

// Deterministic seeds so the stars stay perfectly still and don't flicker when the price moves
var int seed1 = 17
var int seed2 = 23

if barstate.islast
    for r = 0 to 9
        for c = 0 to 9
            // Complex pseudo-randomization for a highly natural scatter effect
            int starRand = (r * seed1 + c * seed2) % 12

            string stardust = ""
            color starColor = color.new(color.white, 100)
            string tSize = size.small

            // Organic placement using varied line breaks (\n) and spacing so it doesn't look like a grid
            if starRand == 0
                stardust := "✦"
                starColor := color.new(#babbff, 30) // Soft glowing purple
                tSize := size.normal
            else if starRand == 1
                stardust := " \n\n ★"
                starColor := color.new(color.white, 50)
            else if starRand == 2
                stardust := "      ·"
                starColor := color.new(#ffd1ff, 30) // Faint pink nebula dust
            else if starRand == 3
                stardust := " \n ✦ \n "
                starColor := color.new(#9bf6ff, 30) // Distant cyan twinkle
            else if starRand == 5
                stardust := "★\n\n"
                starColor := color.new(color.white, 60)
                tSize := size.tiny
            else if starRand == 7
                stardust := "   ·   "
                starColor := color.new(#c8b6ff, 40) // Lavender
            else
                // Leave empty cells to create the dark void of deep space
                stardust := ""

            // THE MAGIC TRICK: width=10.0 and height=10.0 forces each cell to stretch.
            // 10 cells x 10% = exactly 100% full screen coverage edge-to-edge!
            table.cell(space, c, r, text=stardust, text_color=starColor, text_size=tSize, width=10.0, height=10.0)
Open Legacy and Galaxy Theme are provided free, as-is, for use on TradingView. Nothing here is financial advice — always test on a paper account before trading live.

Finish the look

Galaxy Theme is visual only. It draws a transparent starfield table over your chart — no signals, no alerts, nothing to configure. For the full effect, pair it with a dark gradient chart background — see Galaxy Background Setup below.
Optional · CosmeticGalaxy Background Setup▼

Galaxy Theme draws the stars — this is the last step to get the deep-space look behind them, straight from TradingView's own chart settings.

Open chart settings

Click the gear icon (⚙️) at the top of your chart, or right-click anywhere on the chart and choose Settings.

Go to the "Canvas" tab

This is where TradingView controls the chart's overall background.

Set Background to "Gradient"

You'll see a background style dropdown — switch it from Solid to Gradient. Two color swatches will appear next to it.

Set the two gradient colors

Set the first color swatch to a dark purple, and the second to pure black. This gives the chart a deep-space fade that Galaxy Theme's stars sit beautifully on top of.

That's the full look. Dark purple-to-black gradient canvas, underneath Galaxy Theme's transparent starfield, underneath Open Legacy's signals and HUD. Three layers, zero clutter.