Indicator · Pine Script v6 · On the chart

Mean Reversion T

by Ye-Yu-Mo· code credited to Jasxu

Mean Reversion T is a dual-engine intraday indicator designed for 15-minute charts to detect mean-reversion and shock-reversal trading opportunities.

View on GitHub

Repaints

GitHub stars3on the repository
Pine versionv6current
Lines of code999
Settings40inputs
LicenceMPL-2.0
Last update2 months ago28 Aug 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

999 lines of Pine Script v6 from MRT.pine, published under MPL-2.0. Keep the author's credit and licence lines when you reuse it.

MRT.pine
//@version=6
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Jasxu · TradingView: https://www.tradingview.com/u/Jasxu/
// GitHub: https://github.com/Ye-Yu-Mo
indicator("Mean Reversion T", overlay = true, max_labels_count = 500)

// ============================================================================
// Mean Reversion T (MR-T) · Production v2.0.0
//
// 推荐周期 / Recommended timeframe : 15 分钟 (15m) —— 脚本内强制校验
//
// 双引擎 / Dual Engine
//   1. Range Reversion   市场适合震荡回归 → 温和 Z 偏离即可观察
//   2. Shock Reversal    市场不一定横盘 → 异常冲击 + 减速/拒绝 + 回归确认
// 共同保护 / Guard        Hard Trend Veto (1H + 15m 斜率与 Efficiency Ratio)
// 生命周期 / Lifecycle    观察 → T买/T空 → T减 → T平
// 异常退出 / Exits        T止损 / 趋势失效 / T超时 / T保本
//
// 定位 / Scope: 纯 indicator,虚拟持仓,不自动交易,不构成投资建议。
// 设计用于 A 股 T+0 日内做 T;换品种/周期前请重新评估全部参数。
//
// 版本 / Version: v2.0.0 —— 生产重构。变更见 CHANGELOG.md。
// ============================================================================


// ============================================================================
// ⑫ 语言与版本 / ⑫ Language & Version
// ============================================================================
// 运行时 UI(面板 / 图表标签 / 报错)随 lang 切换中英文。
// 注意:Pine 的 input 标题/分组、plotshape 的 text、alertcondition 的 message
// 都是编译期 const string,无法用运行时变量切换语言:输入项标签保持中文,
// 警报消息用双语静态文本。只有 TradingView 原生 i18n(script.i18n)能本地化
// 这些 const 上下文,本版本刻意不依赖未经确认的 API。

lang = input.string("zh", "语言 / Language", options = ["zh", "en"], group = "⑫ 语言与版本 / Language & Version")

f_tr(simple string zh, simple string en) =>
    lang == "zh" ? zh : en

string SCRIPT_NAME = "MR-T"
string SCRIPT_VERSION = "2.0.0"


// ============================================================================
// ① 交易方向
// ============================================================================

allowLong = input.bool(true, "允许T买", group = "① 交易方向")
allowShort = input.bool(true, "允许T空", group = "① 交易方向")


// ============================================================================
// ② 均值与 Z-score
// ============================================================================

meanLen = input.int(32, "15m 局部均值 EMA", minval = 10, group = "② 均值与Z-score")
zLen = input.int(32, "Z-score 周期", minval = 10, group = "② 均值与Z-score")

rangeEntryZ = input.float(1.65, "Range模式观察 Z", minval = 1.0, step = 0.05, group = "② 均值与Z-score")
shockEntryZ = input.float(2.00, "Shock模式观察 Z", minval = 1.2, step = 0.05, group = "② 均值与Z-score")

rangePartialZ = input.float(0.80, "Range T减目标 Z", minval = 0.10, step = 0.05, group = "② 均值与Z-score")
rangeExitZ = input.float(0.25, "Range T平目标 Z", minval = 0.0, step = 0.05, group = "② 均值与Z-score")

shockPartialZ = input.float(1.00, "Shock T减目标 Z", minval = 0.10, step = 0.05, group = "② 均值与Z-score")
shockExitZ = input.float(0.50, "Shock T平目标 Z", minval = 0.0, step = 0.05, group = "② 均值与Z-score")

stopZ = input.float(3.25, "极端统计失效 Z", minval = 2.2, step = 0.25, group = "② 均值与Z-score")


// ============================================================================
// ③ 1H 市场状态
// ============================================================================

htfTf = input.timeframe("60", "市场状态周期", group = "③ 1H 市场状态")
htfMeanLen = input.int(20, "1H 均值 EMA", minval = 5, group = "③ 1H 市场状态")
htfSlopeLookback = input.int(4, "1H 斜率周期", minval = 1, group = "③ 1H 市场状态")

idealHtfSlope = input.float(0.10, "1H 理想最大斜率 ATR/Bar", minval = 0.01, step = 0.01, group = "③ 1H 市场状态")

htfERLen = input.int(10, "1H Efficiency Ratio 周期", minval = 3, group = "③ 1H 市场状态")
idealHtfER = input.float(0.50, "1H 理想最大 ER", minval = 0.10, maxval = 1.0, step = 0.05, group = "③ 1H 市场状态")


// ============================================================================
// ④ 15m 市场状态
// ============================================================================

localSlopeLookback = input.int(8, "15m 斜率周期", minval = 2, group = "④ 15m 市场状态")
idealLocalSlope = input.float(0.12, "15m 理想最大斜率 ATR/Bar", minval = 0.01, step = 0.01, group = "④ 15m 市场状态")

localERLen = input.int(16, "15m Efficiency Ratio 周期", minval = 3, group = "④ 15m 市场状态")
idealLocalER = input.float(0.60, "15m 理想最大 ER", minval = 0.10, maxval = 1.0, step = 0.05, group = "④ 15m 市场状态")


// ============================================================================
// ⑤ Regime Score
// ============================================================================

rangeMinScore = input.float(50.0, "Range模式最低环境分", minval = 0, maxval = 100, step = 1, group = "⑤ Regime Score")
shockMinScore = input.float(45.0, "Shock模式最低环境分", minval = 0, maxval = 100, step = 1, group = "⑤ Regime Score")

regimeSmoothLen = input.int(3, "环境分平滑周期", minval = 1, maxval = 8, group = "⑤ Regime Score")


// ============================================================================
// ⑥ Hard Trend Veto
// ============================================================================

vetoHtfSlope = input.float(0.18, "1H 趋势禁做斜率", minval = 0.05, step = 0.01, group = "⑥ Hard Trend Veto")
vetoLocalSlope = input.float(0.23, "15m 趋势禁做斜率", minval = 0.05, step = 0.01, group = "⑥ Hard Trend Veto")

vetoHtfER = input.float(0.75, "1H 趋势禁做 ER", minval = 0.50, maxval = 1.0, step = 0.01, group = "⑥ Hard Trend Veto")
vetoLocalER = input.float(0.80, "15m 趋势禁做 ER", minval = 0.50, maxval = 1.0, step = 0.01, group = "⑥ Hard Trend Veto")


// ============================================================================
// ⑦ Shock Engine
// ============================================================================

shockLookback = input.int(2, "Shock 检测 K 数", minval = 1, maxval = 4, group = "⑦ Shock Engine")
minShockATR = input.float(0.80, "最小 Shock 强度 ATR", minval = 0.20, step = 0.05, group = "⑦ Shock Engine")

requireShockDeceleration = input.bool(true, "Shock要求冲击减速", group = "⑦ Shock Engine")
decelerationRatio = input.float(0.85, "Shock减速比例", minval = 0.30, maxval = 1.20, step = 0.05, group = "⑦ Shock Engine")

requireRejection = input.bool(true, "Shock要求拒绝/反向K线", group = "⑦ Shock Engine")

minWickRatio = input.float(0.30, "最低拒绝影线比例", minval = 0.0, maxval = 0.8, step = 0.05, group = "⑦ Shock Engine")


// ============================================================================
// ⑧ Half-Life / Time Stop
// ============================================================================

halfLifeLookback = input.int(80, "Half-Life 样本长度", minval = 30, group = "⑧ Half-Life")

timeStopMultiplier = input.float(3.0, "Time Stop = Half-Life ×", minval = 1.0, step = 0.25, group = "⑧ Half-Life")

minTimeStopBars = input.int(8, "最短 Time Stop", minval = 2, group = "⑧ Half-Life")
maxTimeStopBars = input.int(40, "最长 Time Stop", minval = 8, group = "⑧ Half-Life")
fallbackTimeStopBars = input.int(24, "Half-Life无效时 Time Stop", minval = 4, group = "⑧ Half-Life")


// ============================================================================
// ⑨ Setup / Confirmation
// ============================================================================

rangeSetupBars = input.int(16, "Range Setup 有效 K 数", minval = 2, maxval = 48, group = "⑨ 入场确认")
shockSetupBars = input.int(8, "Shock Setup 有效 K 数", minval = 2, maxval = 24, group = "⑨ 入场确认")

requireCandleConfirm = input.bool(true, "要求K线方向确认", group = "⑨ 入场确认")
requireBandReclaim = input.bool(true, "要求价格重新进入Z区间", group = "⑨ 入场确认")

cooldownBars = input.int(3, "退出后冷却 K 数", minval = 0, maxval = 24, group = "⑨ 入场确认")


// ============================================================================
// ⑩ 风险控制
// ============================================================================

atrLen = input.int(14, "ATR 周期", minval = 1, group = "⑩ 风险控制")

hardStopATR = input.float(2.50, "ATR 紧急止损", minval = 0.50, step = 0.25, group = "⑩ 风险控制")

stopMode = input.string("Balanced", "止损模式", options = ["Tight", "Balanced", "Loose"], group = "⑩ 风险控制")

balancedWeight = input.float(0.50, "Balanced统计止损权重", minval = 0.0, maxval = 1.0, step = 0.05, group = "⑩ 风险控制")

moveStopToBE = input.bool(true, "T减后启用保本保护", group = "⑩ 风险控制")

costBps = input.float(3.0, "每边成本(手续费+滑点) bp", minval = 0.0, step = 0.5, group = "⑩ 风险控制")


// ============================================================================
// ⑪ 显示
// ============================================================================

showBands = input.bool(true, "显示均值回归区间", group = "⑪ 显示")
showBackground = input.bool(true, "显示做T环境", group = "⑪ 显示")
showSetup = input.bool(true, "显示观察信号", group = "⑪ 显示")
showPanel = input.bool(true, "显示状态面板", group = "⑪ 显示")
showTradeLevels = input.bool(true, "显示本轮目标/风险", group = "⑪ 显示")


// ============================================================================
// 参数检查
// ============================================================================

if shockEntryZ <= rangeEntryZ
    runtime.error("Shock模式 Z 必须高于 Range模式 Z。/ Shock mode Z must be higher than Range mode Z.")

if rangeExitZ >= rangePartialZ
    runtime.error("Range T平目标必须小于 Range T减目标。/ Range exit Z must be below Range partial Z.")

if shockExitZ >= shockPartialZ
    runtime.error("Shock T平目标必须小于 Shock T减目标。/ Shock exit Z must be below Shock partial Z.")

if rangePartialZ >= rangeEntryZ
    runtime.error("Range T减目标必须小于 Range观察 Z。/ Range partial Z must be below Range entry Z.")

if shockPartialZ >= shockEntryZ
    runtime.error("Shock T减目标必须小于 Shock观察 Z。/ Shock partial Z must be below Shock entry Z.")

if stopZ <= shockEntryZ
    runtime.error("Stop Z 必须高于 Shock Entry Z。/ Stop Z must be above Shock entry Z.")


// ============================================================================
// 周期保护
// ============================================================================

chartSeconds = timeframe.in_seconds(timeframe.period)
requiredSeconds = timeframe.in_seconds("15")
htfSeconds = timeframe.in_seconds(htfTf)

if chartSeconds != requiredSeconds
    runtime.error("Mean Reversion T 设计用于15分钟图。/ Designed for the 15-minute chart.")

if htfSeconds <= chartSeconds
    runtime.error("高周期必须高于15分钟,默认使用60分钟。/ Higher timeframe must be above 15m. Default is 60m.")


// ============================================================================
// Helper Functions
// ============================================================================

// Kaufman Efficiency Ratio, 路径用平均单根位移近似。
f_er(int length) =>
    float absMove = math.abs(ta.change(close))
    float avgMove = ta.sma(absMove, length)
    float path = avgMove * length
    float net = math.abs(close - close[length])
    path > 0.0 ? net / path : 0.0

// 上一根(高周期)已确认的 ER —— 防重绘。需在 request.security 内求值。
f_erPrev(int length) =>
    f_er(length)[1]

// 归一化斜率:EMA 位移 / ATR / lookback。
f_slopeATR(int emaLength, int atrLength, int lookback) =>
    float m = ta.ema(close, emaLength)
    float a = math.max(ta.atr(atrLength), syminfo.mintick)
    (m - m[lookback]) / a / lookback

// 上一根(高周期)已确认的斜率 —— 防重绘。需在 request.security 内求值。
f_slopeATRPrev(int emaLength, int atrLength, int lookback) =>
    f_slopeATR(emaLength, atrLength, lookback)[1]

// 单维度环境分:25 分满分,随 |value|/ideal 衰减,截断到 [0, 25]。
f_score(float value, float idealLimit) =>
    float ratio = idealLimit > 0.0 ? math.abs(value) / idealLimit : 10.0
    float rawScore = 25.0 * (1.0 - ratio)
    float floorScore = math.max(rawScore, 0.0)
    math.min(floorScore, 25.0)


// ============================================================================
// Mean / ATR
// ============================================================================

mean15 = ta.ema(close, meanLen)
atr15 = ta.atr(atrLen)
safeATR = math.max(atr15, syminfo.mintick)


// ============================================================================
// Residual / Z-score
// ============================================================================

residual = close - mean15
residualStd = ta.stdev(residual, zLen)
safeStd = math.max(nz(residualStd), syminfo.mintick)
z = residual / safeStd


// ============================================================================
// 动态区间
// ============================================================================

upperRangeBand = mean15 + rangeEntryZ * safeStd
lowerRangeBand = mean15 - rangeEntryZ * safeStd

upperShockBand = mean15 + shockEntryZ * safeStd
lowerShockBand = mean15 - shockEntryZ * safeStd

upperStopBand = mean15 + stopZ * safeStd
lowerStopBand = mean15 - stopZ * safeStd


// ============================================================================
// Local Regime (15m)
// ============================================================================

localSlopeATR = (mean15 - mean15[localSlopeLookback]) / safeATR / localSlopeLookback
localER = f_er(localERLen)


// ============================================================================
// Confirmed 1H Regime (防重绘:取上一根已确认的 1H bar 值)
// ============================================================================

htfSlopeATR = request.security(syminfo.tickerid, htfTf, f_slopeATRPrev(htfMeanLen, atrLen, htfSlopeLookback), lookahead = barmerge.lookahead_on)
htfER = request.security(syminfo.tickerid, htfTf, f_erPrev(htfERLen), lookahead = barmerge.lookahead_on)


// ============================================================================
// Regime Score
// ============================================================================

htfSlopeScore = f_score(htfSlopeATR, idealHtfSlope)
htfERScore = f_score(htfER, idealHtfER)
localSlopeScore = f_score(localSlopeATR, idealLocalSlope)
localERScore = f_score(localER, idealLocalER)

rawRegimeScore = htfSlopeScore + htfERScore + localSlopeScore + localERScore
regimeScore = ta.sma(rawRegimeScore, regimeSmoothLen)


// ============================================================================
// Hard Trend Veto
// ============================================================================

htfSlopeVeto = math.abs(htfSlopeATR) >= vetoHtfSlope
localSlopeVeto = math.abs(localSlopeATR) >= vetoLocalSlope
htfERVeto = htfER >= vetoHtfER
localERVeto = localER >= vetoLocalER

hardTrendVeto = htfSlopeVeto or localSlopeVeto or htfERVeto or localERVeto

rangeEnvironment = not hardTrendVeto and regimeScore >= rangeMinScore
shockEnvironment = not hardTrendVeto and regimeScore >= shockMinScore


// ============================================================================
// Shock Detection
// ============================================================================
// Shock 强度:最近 N 根走了多少 ATR。

shockMove = (close - close[shockLookback]) / safeATR
shockUp = shockMove >= minShockATR
shockDown = shockMove <= -minShockATR

// 当前冲击 vs 前一段冲击:开始减弱 → 更像过冲结束。
previousShockMove = (close[shockLookback] - close[shockLookback * 2]) / safeATR
shockDecelerating = math.abs(shockMove) <= math.abs(previousShockMove) * decelerationRatio
decelerationOK = not requireShockDeceleration or shockDecelerating


// ============================================================================
// Candle / Wick Rejection
// ============================================================================

barRange = math.max(high - low, syminfo.mintick)

lowerWick = math.min(open, close) - low
upperWick = high - math.max(open, close)

lowerWickRatio = lowerWick / barRange
upperWickRatio = upperWick / barRange

bullishCandle = close > open
bearishCandle = close < open

longRejection = bullishCandle or lowerWickRatio >= minWickRatio
shortRejection = bearishCandle or upperWickRatio >= minWickRatio

longRejectionOK = not requireRejection or longRejection
shortRejectionOK = not requireRejection or shortRejection


// ============================================================================
// Half-Life
// ============================================================================
// 对 d(residual) ~ beta * residual[-1] 回归,Half-Life = -ln(2) / beta。
// beta 为正(动量)或无效时,退化到 fallbackTimeStopBars。

residualLag = residual[1]
residualDelta = residual - residual[1]

hlCorr = ta.correlation(residualDelta, residualLag, halfLifeLookback)
hlDeltaStd = ta.stdev(residualDelta, halfLifeLookback)
hlLagStd = ta.stdev(residualLag, halfLifeLookback)

bool betaOK = not na(hlCorr) and not na(hlDeltaStd) and not na(hlLagStd) and hlLagStd > 0.0

float hlBeta = na

if betaOK
    hlBeta := hlCorr * hlDeltaStd / hlLagStd

float halfLife = na

if not na(hlBeta) and hlBeta < -0.000001
    halfLife := -math.log(2.0) / hlBeta


// ============================================================================
// 全局 Cross
// ============================================================================

zCrossOverNegRange = ta.crossover(z, -rangeEntryZ)
zCrossUnderPosRange = ta.crossunder(z, rangeEntryZ)

zCrossOverNegShock = ta.crossover(z, -shockEntryZ)
zCrossUnderPosShock = ta.crossunder(z, shockEntryZ)


// ============================================================================
// State
// ============================================================================
// 所有可变状态收敛到一个对象,方向用 ±1 表达。
// 多空镜像逻辑由 f_enter / f_exit / f_manage 以 dir 参数统一,消除复制粘贴。

type MRState
    int    pos           = 0     // 0 空仓 / +1 多头 / -1 空头
    bool   partialTaken  = false
    int    setupBar      = na    // 活跃 Setup 的 bar_index
    int    setupDir      = 0     // Setup 方向:+1 / -1
    int    setupMode     = 0     // 0 无 / 1 Range / 2 Shock
    int    entryBar      = na
    float  entryPrice    = na
    float  mean          = na
    float  std           = na
    float  atr           = na
    float  partialTarget = na
    float  finalTarget   = na
    float  baseStop      = na
    int    timeStop      = na
    int    tradeMode     = 0     // 0 无 / 1 Range / 2 Shock
    float  cost          = 0.0   // 往返成本(价格单位)
    int    lastExitBar   = na
    int    eventKind     = 0     // 本 bar 事件:0 无 / 1 进场 / 2 T减 / 3 退出
    int    eventDir      = 0     // 事件方向:+1 / -1
    int    eventReason   = 0     // 退出原因(仅 eventKind == 3)
    int    tradeCount    = 0
    float  sumPnlATR     = 0.0
    float  lastPnlATR    = na
    float  lastPnlPct    = na

var MRState t = MRState.new()

// 一次性事件:Pine 禁止在函数内重赋任何全局变量(CE10088),函数只能改对象字段。
// 因此事件先写入 t 的捕获字段,全局作用域再派生为这些变量。
bool setupEvent      = false
int  setupEventDir   = 0
int  setupEventMode  = 0

bool entryEvent      = false
int  entryEventDir   = 0

bool partialEvent    = false
int  partialEventDir = 0

bool exitEvent       = false
int  exitEventDir    = 0
int  exitReason      = 0

// 每 bar 重置 t 的事件捕获字段(函数通过 t 传出事件)
t.eventKind := 0
t.eventDir := 0
t.eventReason := 0

// 退出原因枚举
int R_STOP  = 1
int R_TREND = 2
int R_TIME  = 3
int R_FINAL = 4
int R_BE    = 5

// 冷却
int barsSinceExit = 100000

if not na(t.lastExitBar)
    barsSinceExit := bar_index - t.lastExitBar

requiredCooldown = math.max(cooldownBars, 1)
canEnter = na(t.lastExitBar) or barsSinceExit >= requiredCooldown


// ============================================================================
// 创建 Setup (Shock 优先于 Range;方向反转允许替换对侧 Setup)
// ============================================================================

if barstate.isconfirmed and t.pos == 0 and canEnter

    bool noSameLong  = na(t.setupBar) or t.setupDir != 1
    bool noSameShort = na(t.setupBar) or t.setupDir != -1

    bool longShockSetup = allowLong and shockEnvironment and shockDown and z <= -shockEntryZ and decelerationOK and longRejectionOK and noSameLong
    bool shortShockSetup = allowShort and shockEnvironment and shockUp and z >= shockEntryZ and decelerationOK and shortRejectionOK and noSameShort
    bool longRangeSetup = allowLong and rangeEnvironment and z <= -rangeEntryZ and noSameLong
    bool shortRangeSetup = allowShort and rangeEnvironment and z >= rangeEntryZ and noSameShort

    if longShockSetup
        t.setupBar := bar_index
        t.setupDir := 1
        t.setupMode := 2
        setupEvent := true
        setupEventDir := 1
        setupEventMode := 2

    else if shortShockSetup
        t.setupBar := bar_index
        t.setupDir := -1
        t.setupMode := 2
        setupEvent := true
        setupEventDir := -1
        setupEventMode := 2

    else if longRangeSetup
        t.setupBar := bar_index
        t.setupDir := 1
        t.setupMode := 1
        setupEvent := true
        setupEventDir := 1
        setupEventMode := 1

    else if shortRangeSetup
        t.setupBar := bar_index
        t.setupDir := -1
        t.setupMode := 1
        setupEvent := true
        setupEventDir := -1
        setupEventMode := 1


// ============================================================================
// Setup Expiry
// ============================================================================

if not na(t.setupBar)
    int allowedAge = t.setupMode == 2 ? shockSetupBars : rangeSetupBars

    if bar_index - t.setupBar > allowedAge
        t.setupBar := na
        t.setupDir := 0
        t.setupMode := 0


// ============================================================================
// Setup 提前失败 (Z 走极端)
// ============================================================================

if not na(t.setupBar) and t.setupDir * z <= -stopZ
    t.setupBar := na
    t.setupDir := 0
    t.setupMode := 0


// ============================================================================
// Entry Confirmation
// ============================================================================

float entryZ = t.setupMode == 2 ? shockEntryZ : rangeEntryZ

bool candleOK = false

if t.setupDir == 1
    candleOK := not requireCandleConfirm or close > close[1]
else if t.setupDir == -1
    candleOK := not requireCandleConfirm or close < close[1]

bool zReclaim = false

if t.setupDir == 1
    zReclaim := t.setupMode == 2 ? zCrossOverNegShock : zCrossOverNegRange
else if t.setupDir == -1
    zReclaim := t.setupMode == 2 ? zCrossUnderPosShock : zCrossUnderPosRange

bool bandOK = false

if t.setupDir == 1
    bandOK := not requireBandReclaim or close > mean15 - entryZ * safeStd
else if t.setupDir == -1
    bandOK := not requireBandReclaim or close < mean15 + entryZ * safeStd

bool longEntrySignal = barstate.isconfirmed and t.pos == 0 and not hardTrendVeto and t.setupDir == 1 and not na(t.setupBar) and zReclaim and bandOK and candleOK
bool shortEntrySignal = barstate.isconfirmed and t.pos == 0 and not hardTrendVeto and t.setupDir == -1 and not na(t.setupBar) and zReclaim and bandOK and candleOK


// ============================================================================
// 活跃止损 (含 T减后保本偏移 —— 单一事实来源,管理逻辑与绘图共用)
// ============================================================================

f_activeStop(int dir) =>
    float s = t.baseStop

    if moveStopToBE and t.partialTaken
        s := dir == 1 ? math.max(s, t.entryPrice + t.cost) : math.min(s, t.entryPrice - t.cost)

    s


// ============================================================================
// Enter (方向参数化,dir = +1 多 / -1 空)
// ============================================================================

f_enter(int dir) =>
    t.pos := dir
    t.partialTaken := false
    t.entryPrice := close
    t.mean := mean15
    t.std := safeStd
    t.atr := safeATR
    t.entryBar := bar_index

    bool shock = t.setupMode == 2
    t.tradeMode := shock ? 2 : 1

    float pz = shock ? shockPartialZ : rangePartialZ
    float ez = shock ? shockExitZ : rangeExitZ

    // 目标价统一表达为 mean - dir * Z * std
    t.partialTarget := t.mean - dir * pz * t.std
    t.finalTarget   := t.mean - dir * ez * t.std

    float atrStop   = t.entryPrice - dir * hardStopATR * t.atr
    float statStop  = t.mean - dir * stopZ * t.std
    float tightStop = dir == 1 ? math.max(atrStop, statStop) : math.min(atrStop, statStop)
    float looseStop = dir == 1 ? math.min(atrStop, statStop) : math.max(atrStop, statStop)
    float balStop   = looseStop + (tightStop - looseStop) * balancedWeight

    if stopMode == "Tight"
        t.baseStop := tightStop
    else if stopMode == "Loose"
        t.baseStop := looseStop
    else
        t.baseStop := balStop

    float rawTimeStop = fallbackTimeStopBars

    if not na(halfLife)
        rawTimeStop := halfLife * timeStopMultiplier

    float limitedTimeStop = math.max(minTimeStopBars, rawTimeStop)
    limitedTimeStop := math.min(maxTimeStopBars, limitedTimeStop)

    t.timeStop := int(math.round(limitedTimeStop))

    // 往返成本(价格单位),用于保本偏移与 P&L 结算
    t.cost := t.entryPrice * (2.0 * costBps) / 10000.0

    t.setupBar := na
    t.setupDir := 0
    t.setupMode := 0

    t.eventKind := 1
    t.eventDir := dir


// ============================================================================
// Exit (方向参数化;结算虚拟 P&L,扣除往返成本)
// ============================================================================

f_exit(int dir, int reason) =>
    float rawPnl = (close - t.entryPrice) * dir
    float costVal = t.entryPrice * (2.0 * costBps) / 10000.0
    float pnlNet = rawPnl - costVal

    t.lastPnlATR := pnlNet / t.atr
    t.lastPnlPct := pnlNet / t.entryPrice * 100.0
    t.sumPnlATR += t.lastPnlATR
    t.tradeCount += 1

    t.lastExitBar := bar_index
    t.pos := 0
    t.partialTaken := false

    t.entryBar := na
    t.entryPrice := na
    t.mean := na
    t.std := na
    t.atr := na
    t.partialTarget := na
    t.finalTarget := na
    t.baseStop := na
    t.timeStop := na
    t.tradeMode := 0
    t.cost := 0.0

    t.setupBar := na
    t.setupDir := 0
    t.setupMode := 0

    t.eventKind := 3
    t.eventDir := dir
    t.eventReason := reason


// ============================================================================
// Manage (方向参数化)
// ============================================================================
// 退出优先级:止损 → 趋势失效 → 超时 → T减 → T平。
// 关键修复:T平(T final)以「先 T减」为前提,同根 K 线同时触达 T减/T平时,
// 先记 T减,下一根才允许 T平 —— 生命周期不再跳级。

f_manage(int dir) =>
    bool canManage = t.pos == dir and not na(t.entryBar) and bar_index > t.entryBar

    if barstate.isconfirmed and canManage
        float activeStop = f_activeStop(dir)

        bool stopHit = not na(activeStop) and (dir == 1 ? close <= activeStop : close >= activeStop)
        bool trendFailed = dir * htfSlopeATR <= -vetoHtfSlope or dir * localSlopeATR <= -vetoLocalSlope
        bool timeFailed = not na(t.timeStop) and (bar_index - t.entryBar) >= t.timeStop
        bool partialReached = not t.partialTaken and not na(t.partialTarget) and (dir == 1 ? close >= t.partialTarget : close <= t.partialTarget)
        bool finalReached = t.partialTaken and not na(t.finalTarget) and (dir == 1 ? close >= t.finalTarget : close <= t.finalTarget)

        if stopHit
            // T减后且启用保本 → 保本退出;否则统计/硬止损退出
            bool isBE = t.partialTaken and moveStopToBE

            if isBE
                f_exit(dir, R_BE)
            else
                f_exit(dir, R_STOP)

        else if trendFailed
            f_exit(dir, R_TREND)

        else if timeFailed
            f_exit(dir, R_TIME)

        else if partialReached
            t.partialTaken := true
            t.eventKind := 2
            t.eventDir := dir

        else if finalReached
            f_exit(dir, R_FINAL)


// ============================================================================
// 执行入口 / 管理
// ============================================================================

if longEntrySignal
    f_enter(1)
else if shortEntrySignal
    f_enter(-1)

f_manage(1)
f_manage(-1)

// 派生一次性显示事件(全局作用域写全局变量是允许的)
entryEvent := t.eventKind == 1
entryEventDir := t.eventDir
partialEvent := t.eventKind == 2
partialEventDir := t.eventDir
exitEvent := t.eventKind == 3
exitEventDir := t.eventDir
exitReason := t.eventReason


// ============================================================================
// Plot Mean
// ============================================================================

plot(mean15, "Local Mean", color = color.yellow, linewidth = 2)


// ============================================================================
// Bands
// ============================================================================

pRangeUpper = plot(showBands ? upperRangeBand : na, "Range +Z", color = color.new(color.orange, 35))
pRangeLower = plot(showBands ? lowerRangeBand : na, "Range -Z", color = color.new(color.aqua, 35))

pShockUpper = plot(showBands ? upperShockBand : na, "Shock +Z", color = color.new(color.red, 35))
pShockLower = plot(showBands ? lowerShockBand : na, "Shock -Z", color = color.new(color.green, 35))

pStopUpper = plot(showBands ? upperStopBand : na, "Extreme +Z", color = color.new(color.red, 75))
pStopLower = plot(showBands ? lowerStopBand : na, "Extreme -Z", color = color.new(color.red, 75))

fill(pShockUpper, pStopUpper, color = showBands ? color.new(color.red, 94) : na)
fill(pShockLower, pStopLower, color = showBands ? color.new(color.green, 94) : na)


// ============================================================================
// Background
// ============================================================================

color bgCol = na

if showBackground
    if rangeEnvironment
        bgCol := color.new(color.blue, 92)
    else if shockEnvironment
        bgCol := color.new(color.purple, 94)

bgcolor(bgCol, title = "Mean Reversion Environment")


// ============================================================================
// Active Trade Lines
// ============================================================================

float activePartialLine = na
float activeFinalLine = na
float activeRiskLine = na

if t.pos != 0
    activePartialLine := t.partialTarget
    activeFinalLine := t.finalTarget
    activeRiskLine := f_activeStop(t.pos)

plot(showTradeLevels ? activePartialLine : na, "T减目标 / Trim", color = color.orange, linewidth = 2, style = plot.style_linebr)
plot(showTradeLevels ? activeFinalLine : na, "T平目标 / Target", color = color.white, linewidth = 2, style = plot.style_linebr)
plot(showTradeLevels ? activeRiskLine : na, "风险线 / Risk", color = color.red, linewidth = 2, style = plot.style_linebr)


// ============================================================================
// Setup Labels
// ============================================================================
// 用 label.new 而非 plotshape:plotshape 的 text 是 const string,无法随
// lang 切换;label.new 的 text 是 series string,可接 f_tr()。

if showSetup and setupEvent and setupEventMode == 1 and setupEventDir == 1
    label.new(bar_index, low, f_tr("R观察", "R obs"), style = label.style_label_up, color = color.new(color.green, 55), textcolor = color.white, size = size.tiny)

if showSetup and setupEvent and setupEventMode == 1 and setupEventDir == -1
    label.new(bar_index, high, f_tr("R观察", "R obs"), style = label.style_label_down, color = color.new(color.red, 55), textcolor = color.white, size = size.tiny)

if showSetup and setupEvent and setupEventMode == 2 and setupEventDir == 1
    label.new(bar_index, low, f_tr("S观察", "S obs"), style = label.style_label_up, color = color.green, textcolor = color.white, size = size.tiny)

if showSetup and setupEvent and setupEventMode == 2 and setupEventDir == -1
    label.new(bar_index, high, f_tr("S观察", "S obs"), style = label.style_label_down, color = color.red, textcolor = color.white, size = size.tiny)


// ============================================================================
// Entry
// ============================================================================

if entryEvent and entryEventDir == 1
    label.new(bar_index, low, f_tr("T买", "T Long"), style = label.style_label_up, color = color.green, textcolor = color.white, size = size.small)

if entryEvent and entryEventDir == -1
    label.new(bar_index, high, f_tr("T空", "T Short"), style = label.style_label_down, color = color.red, textcolor = color.white, size = size.small)


// ============================================================================
// Partial
// ============================================================================

if partialEvent and partialEventDir == 1
    label.new(bar_index, high, f_tr("T减", "T Trim"), style = label.style_label_down, color = color.orange, textcolor = color.white, size = size.tiny)

if partialEvent and partialEventDir == -1
    label.new(bar_index, low, f_tr("T减", "T Trim"), style = label.style_label_up, color = color.aqua, textcolor = color.black, size = size.tiny)


// ============================================================================
// Exits (Final / Stop / Trend / Time / BE)
// ============================================================================

if exitEvent and exitReason == R_FINAL and exitEventDir == 1
    label.new(bar_index, high, f_tr("T平", "T Close"), style = label.style_label_down, color = color.lime, textcolor = color.black, size = size.small)

if exitEvent and exitReason == R_FINAL and exitEventDir == -1
    label.new(bar_index, low, f_tr("T平", "T Close"), style = label.style_label_up, color = color.lime, textcolor = color.black, size = size.small)

if exitEvent and exitReason == R_STOP and exitEventDir == 1
    label.new(bar_index, high, f_tr("T止损", "T Stop"), style = label.style_label_down, color = color.red, textcolor = color.white, size = size.small)

if exitEvent and exitReason == R_STOP and exitEventDir == -1
    label.new(bar_index, low, f_tr("T止损", "T Stop"), style = label.style_label_up, color = color.red, textcolor = color.white, size = size.small)

if exitEvent and exitReason == R_TREND and exitEventDir == 1
    label.new(bar_index, high, f_tr("趋势失效", "Trend fail"), style = label.style_label_down, color = color.purple, textcolor = color.white, size = size.small)

if exitEvent and exitReason == R_TREND and exitEventDir == -1
    label.new(bar_index, low, f_tr("趋势失效", "Trend fail"), style = label.style_label_up, color = color.purple, textcolor = color.white, size = size.small)

if exitEvent and exitReason == R_TIME and exitEventDir == 1
    label.new(bar_index, high, f_tr("T超时", "T Timeout"), style = label.style_label_down, color = color.gray, textcolor = color.white, size = size.small)

if exitEvent and exitReason == R_TIME and exitEventDir == -1
    label.new(bar_index, low, f_tr("T超时", "T Timeout"), style = label.style_label_up, color = color.gray, textcolor = color.white, size = size.small)

if exitEvent and exitReason == R_BE and exitEventDir == 1
    label.new(bar_index, high, f_tr("T保本", "T BE"), style = label.style_label_down, color = color.gray, textcolor = color.white, size = size.tiny)

if exitEvent and exitReason == R_BE and exitEventDir == -1
    label.new(bar_index, low, f_tr("T保本", "T BE"), style = label.style_label_up, color = color.gray, textcolor = color.white, size = size.tiny)


// ============================================================================
// Data Window
// ============================================================================

plot(z, "Z-score", display = display.data_window)
plot(regimeScore, "Regime Score", display = display.data_window)
plot(halfLife, "Half-Life", display = display.data_window)
plot(shockMove, "Shock ATR", display = display.data_window)
plot(previousShockMove, "Previous Shock ATR", display = display.data_window)
plot(localER, "15m ER", display = display.data_window)
plot(htfER, "1H ER", display = display.data_window)
plot(localSlopeATR, "15m Slope", display = display.data_window)
plot(htfSlopeATR, "1H Slope", display = display.data_window)
plot(t.sumPnlATR, "Cum P&L (ATR)", display = display.data_window)


// ============================================================================
// Panel
// ============================================================================

var table panel = table.new(position.top_right, 2, 12, border_width = 1)

if showPanel and barstate.islast

    string envText = f_tr("禁做T", "No T")
    color envColor = color.gray

    if rangeEnvironment
        envText := "Range"
        envColor := color.aqua
    else if shockEnvironment
        envText := "Shock Only"
        envColor := color.purple

    string stateText = t.pos == 1 ? f_tr("T多", "Long") : t.pos == -1 ? f_tr("T空", "Short") : f_tr("空仓", "Flat")

    string setupText = f_tr("无", "None")

    if t.setupMode == 1 and t.setupDir == 1
        setupText := f_tr("R等T买", "Range→Long")
    else if t.setupMode == 2 and t.setupDir == 1
        setupText := f_tr("S等T买", "Shock→Long")
    else if t.setupMode == 1 and t.setupDir == -1
        setupText := f_tr("R等T空", "Range→Short")
    else if t.setupMode == 2 and t.setupDir == -1
        setupText := f_tr("S等T空", "Shock→Short")

    string hlText = na(halfLife) ? f_tr("无效", "n/a") : str.tostring(halfLife, "#.0")
    string shockText = str.tostring(shockMove, "#.00") + " ATR"
    string scoreText = str.tostring(regimeScore, "#")
    string modeText = t.pos == 0 ? "-" : t.tradeMode == 2 ? "Shock" : "Range"
    string stopText = stopMode

    string pnlText = na(t.lastPnlATR) ? "-" : str.tostring(t.lastPnlATR, "#.00") + " ATR / " + str.tostring(t.lastPnlPct, "#.00") + "%"
    string cumText = str.tostring(t.sumPnlATR, "#.00") + " ATR (" + str.tostring(t.tradeCount, "#") + f_tr("笔", " trades") + ")"

    table.cell(panel, 0, 0, SCRIPT_NAME + " v" + SCRIPT_VERSION)
    table.cell(panel, 1, 0, "15m")

    table.cell(panel, 0, 1, f_tr("环境", "Env"))
    table.cell(panel, 1, 1, envText, text_color = envColor)

    table.cell(panel, 0, 2, "Score")
    table.cell(panel, 1, 2, scoreText)

    table.cell(panel, 0, 3, f_tr("状态", "State"))
    table.cell(panel, 1, 3, stateText)

    table.cell(panel, 0, 4, "Setup")
    table.cell(panel, 1, 4, setupText)

    table.cell(panel, 0, 5, "Z")
    table.cell(panel, 1, 5, str.tostring(z, "#.00"))

    table.cell(panel, 0, 6, "Shock")
    table.cell(panel, 1, 6, shockText)

    table.cell(panel, 0, 7, "Half-Life")
    table.cell(panel, 1, 7, hlText)

    table.cell(panel, 0, 8, f_tr("模式", "Mode"))
    table.cell(panel, 1, 8, modeText)

    table.cell(panel, 0, 9, "Stop")
    table.cell(panel, 1, 9, stopText)

    table.cell(panel, 0, 10, f_tr("本笔P&L", "Last P&L"))
    table.cell(panel, 1, 10, pnlText)

    table.cell(panel, 0, 11, f_tr("累计P&L", "Cum P&L"))
    table.cell(panel, 1, 11, cumText)


if not showPanel and barstate.islast
    table.clear(panel, 0, 0, 1, 11)


// ============================================================================
// Alerts
// ============================================================================

alertcondition(setupEvent and setupEventMode == 1, "MR-T Range观察 / Range obs", "Mean Reversion T: Range模式出现均值偏离 / mean deviation in Range mode")
alertcondition(setupEvent and setupEventMode == 2, "MR-T Shock观察 / Shock obs", "Mean Reversion T: 出现异常价格冲击并开始减速 / abnormal shock with deceleration")
alertcondition(entryEvent and entryEventDir == 1, "MR-T T买 / T Long", "Mean Reversion T: 出现T买确认 / long entry confirmed")
alertcondition(entryEvent and entryEventDir == -1, "MR-T T空 / T Short", "Mean Reversion T: 出现T空确认 / short entry confirmed")
alertcondition(partialEvent, "MR-T T减 / T Trim", "Mean Reversion T: 完成第一阶段回归 / first-stage reversion done")
alertcondition(exitEvent and exitReason == R_FINAL, "MR-T T平 / T Close", "Mean Reversion T: 主要均值回归完成 / main reversion complete")
alertcondition(exitEvent and exitReason == R_STOP, "MR-T T止损 / T Stop", "Mean Reversion T: 触发风险线 / risk line hit")
alertcondition(exitEvent and exitReason == R_TREND, "MR-T 趋势失效 / Trend fail", "Mean Reversion T: 市场已发展为趋势环境 / market turned trending")
alertcondition(exitEvent and exitReason == R_TIME, "MR-T T超时 / T Timeout", "Mean Reversion T: 回归时间超过预期 / reversion timed out")
alertcondition(exitEvent and exitReason == R_BE, "MR-T T保本 / T BE", "Mean Reversion T: 第一目标后触发保本保护 / BE protection after first target")

Overview

Mean Reversion T calculates Z-score price deviations from a moving average mean alongside regime scores across 15-minute and 1-hour timeframes. To filter out bad setups, hard trend vetoes block signals when strong slopes or high efficiency ratios exist. The script features an Ornstein-Uhlenbeck process half-life time exit, cost-aware virtual P&L tracking, and an on-screen dashboard display.

Details

Published by

Ye-Yu-Mo on GitHub

Code credit

Jasxu, as written in the script's header

Draws

On the price chart

Markets

Stocks

Timeframes

M15-H1

Risk Notes

  • Reads higher-timeframe data with lookahead on, so the history can show signals that were not there in real time

Topics

a-sharesalgorithmic-tradingindicatorintraday-tradingmean-reversionmean-reversion-tradingpinescriptpinescript-v6quantitative-financet0-tradingtechnical-analysistrading-indicatortrading-strategytradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
语言 / Languagestringzhlang
允许T买booltrueallowLong
允许T空booltrueallowShort
15m 局部均值 EMAint32meanLen
Z-score 周期int32zLen
Range模式观察 Zfloat1.65rangeEntryZ
Shock模式观察 Zfloat2.00shockEntryZ
Range T减目标 Zfloat0.80rangePartialZ
Range T平目标 Zfloat0.25rangeExitZ
Shock T减目标 Zfloat1.00shockPartialZ
Shock T平目标 Zfloat0.50shockExitZ
极端统计失效 Zfloat3.25stopZ
Show all 40 settings
SettingTypeDefaultVariable
市场状态周期timeframe60htfTf
1H 均值 EMAint20htfMeanLen
1H 斜率周期int4htfSlopeLookback
1H 理想最大斜率 ATR/Barfloat0.10idealHtfSlope
1H Efficiency Ratio 周期int10htfERLen
1H 理想最大 ERfloat0.50idealHtfER
15m 斜率周期int8localSlopeLookback
15m 理想最大斜率 ATR/Barfloat0.12idealLocalSlope
15m Efficiency Ratio 周期int16localERLen
15m 理想最大 ERfloat0.60idealLocalER
Range模式最低环境分float50.0rangeMinScore
Shock模式最低环境分float45.0shockMinScore
环境分平滑周期int3regimeSmoothLen
1H 趋势禁做斜率float0.18vetoHtfSlope
15m 趋势禁做斜率float0.23vetoLocalSlope
1H 趋势禁做 ERfloat0.75vetoHtfER
15m 趋势禁做 ERfloat0.80vetoLocalER
Shock 检测 K 数int2shockLookback
最小 Shock 强度 ATRfloat0.80minShockATR
Shock要求冲击减速booltruerequireShockDeceleration
Shock减速比例float0.85decelerationRatio
Shock要求拒绝/反向K线booltruerequireRejection
最低拒绝影线比例float0.30minWickRatio
Half-Life 样本长度int80halfLifeLookback
Time Stop = Half-Life ×float3.0timeStopMultiplier
最短 Time Stopint8minTimeStopBars
最长 Time Stopint40maxTimeStopBars
Half-Life无效时 Time Stopint24fallbackTimeStopBars
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

114 stars

Calculates normalized reversal metrics across up to ten Forex currency pairs using user-selected moving average methods, including HarmonicMA, EMA, SMA, or WMA. The indicator identifies overbought, oversold, and mean-crossing conditions using a configurable th

Indicator v4 Screeners & dashboardsMean reversion
Updated 1 year ago · None stated GitHub repository of Multi-Pair Trend Reversal Widget

This overlay indicator plots a Supertrend from an ATR length and factor, four exponential moving averages, and a selectable VWAP or TWAP taken from a chosen timeframe on the price chart. It also shows MACD and stochastic with adjustable lengths, smoothing, ver

Indicator v5 Oscillators & momentumMoving averages
Updated 1 year ago · MPL-2.0 GitHub repository of Momentum Setup: Vijay Thakare Option Buying Scalping Setup

This indicator plots trade entry and exit signals on the price chart by combining Bollinger Bands with optional filter conditions. It calculates standard Bollinger Bands along with RSI, Aroon, and moving average filters. Reversal signals trigger when price bre

Indicator v4 Volatility & bandsOscillators & momentum
Updated 2 months ago · None stated GitHub repository of Bollinger Bands Winner IND [Alorse]
285 stars

Mean Reversion Study is an indicator that identifies trade setups combining exponential moving averages, RSI extremes, and engulfing candlestick patterns. It highlights bullish setups when a bullish engulfing candle forms below the EMA while RSI is oversold, a

Indicator v4 Mean reversionCandlestick patterns
Updated 3 years ago · None stated GitHub repository of Mean Reversion Study
257 stars

This indicator plots a 200-period weighted moving average (WMA) on the main chart and monitors price deviations across multiple timeframes. It retrieves daily, 15-minute, 5-minute, and 2-minute price data to track distance from the 200 WMA relative to key sess

Indicator v5 Moving averagesSessions & time Repaints
Updated 7 weeks ago · None stated GitHub repository of Donald SPX - Options Trading
228 stars

TradersPost Trading Checklist is an overlay indicator that evaluates a suite of popular technical conditions for long and short trade setups. It checks session-anchored VWAP, fast and slow EMA alignments and crosses, 200 SMA, RSI overbought or oversold conditi

Indicator v6 Screeners & dashboardsTrend
Updated 3 months ago · None stated GitHub repository of TradersPost Trading Checklist

More Mean reversion scripts

Related on ForexR

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers