Hi everyone,
If you've been around the markets as long as I have, you know that high-volume scalping requires a completely different approach to risk management. When we are pushing heavy lot sizes, floating drawdown can breach our risk thresholds long before a candlestick actually closes. Relying on standard end-of-bar calculations just doesn't cut it.
To solve this, I’ve just finished coding a professional-grade Equity Drawdown & Risk Engine in Pine Script v5, and I want to share it with the community here.
Unlike standard scripts that track static price action, this tool is built to monitor live strategy.equity on every single tick. It incorporates a hard kill-switch designed to safeguard your capital when automated strategies go off the rails.
I'll drop the full open-source code below. Feel free to bolt this module onto your existing automated strategies. Drop a reply if you run into any compilation errors or have requests for additional metrics. Let's keep the discussion going.
Trade safe,
Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
Here is an extended real-time drawdown calculator built in Pine Script v5. It is designed to run in a separate pane below your chart and includes advanced dashboard metrics for tracking risk.
Code: Select all
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pavel Tuček | forex-scalping.com
//@version=5
indicator("Extended Portfolio Drawdown Calculator", overlay=false, precision=2)
// ========================================================================= //
// ============================= INPUTS ==================================== //
// ========================================================================= //
grp_main = "Main Settings"
src = input.source(close, title="Portfolio/Asset Source", group=grp_main)
mode = input.string("All Time", title="Calculation Mode", options=["All Time", "Rolling Window"], group=grp_main)
window = input.int(252, title="Rolling Window (Bars)", minval=1, tooltip="Used only if 'Rolling Window' is selected.", group=grp_main)
grp_ui = "UI & Visuals"
critDd = input.float(20.0, title="Critical Drawdown Threshold (%)", step=1.0, group=grp_ui)
showTable = input.bool(true, title="Show Dashboard Panel", group=grp_ui)
tablePos = input.string(position.top_right, title="Dashboard Position", options=[position.top_right, position.top_left, position.bottom_right, position.bottom_left], group=grp_ui)
// ========================================================================= //
// =========================== CALCULATIONS ================================ //
// ========================================================================= //
var float allTimePeak = na
var float maxDdAllTime = 0.0
var int peakBarIndex = na
float currentPeak = na
// Determine Peak based on mode
if mode == "All Time"
if na(allTimePeak) or src >= allTimePeak
allTimePeak := src
peakBarIndex := bar_index
currentPeak := allTimePeak
else
currentPeak := ta.highest(src, window)
// Calculate Current Drawdown
float currentDd = currentPeak > 0 ? ((src - currentPeak) / currentPeak) * 100 : 0.0
// Calculate Max Drawdown
float maxDd = na
if mode == "All Time"
maxDdAllTime := math.min(maxDdAllTime, currentDd)
maxDd := maxDdAllTime
else
maxDd := ta.lowest(currentDd, window)
// Calculate Required Recovery %
float recoveryReq = currentPeak > 0 and src > 0 ? ((currentPeak / src) - 1.0) * 100.0 : 0.0
// Calculate Drawdown Duration (bars)
int ddDuration = na
if mode == "All Time"
ddDuration := bar_index - peakBarIndex
else
ddDuration := int(math.abs(ta.highestbars(src, window)))
// ========================================================================= //
// ============================= PLOTTING ================================== //
// ========================================================================= //
// Dynamic color for the drawdown area
color ddColor = currentDd <= -critDd ? color.new(color.red, 50) : color.new(color.orange, 50)
plot(currentDd, title="Current Drawdown", color=ddColor, style=plot.style_area, linewidth=1)
plot(maxDd, title="Max Drawdown", color=color.red, style=plot.style_line, linewidth=2)
hline(0, title="Zero Line", color=color.gray, linestyle=hline.style_dashed)
hline(-critDd, title="Critical Level", color=color.new(color.red, 30), linestyle=hline.style_dotted)
// ========================================================================= //
// ======================== DASHBOARD TABLE ================================ //
// ========================================================================= //
if showTable
var table dash = table.new(tablePos, 2, 7, border_width=1, border_color=color.new(color.gray, 80), frame_color=color.new(color.gray, 80), frame_width=1)
if barstate.islast
color bgHead = color.new(color.black, 20)
color bgCell = color.new(color.black, 50)
color textVal = color.white
color alertText = currentDd <= -critDd ? color.red : color.white
// Headers
table.cell(dash, 0, 0, "Metric", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_left)
table.cell(dash, 1, 0, "Value", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_right)
// Rows
table.cell(dash, 0, 1, "Peak Value", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 1, str.tostring(currentPeak, "#.##"), text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 2, "Current Value", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 2, str.tostring(src, "#.##"), text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 3, "Current Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 3, str.tostring(currentDd, "#.##") + " %", text_color=alertText, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 4, "Required Recovery", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 4, str.tostring(recoveryReq, "#.##") + " %", text_color=color.yellow, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 5, "Max Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 5, str.tostring(maxDd, "#.##") + " %", text_color=color.red, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 6, "Drawdown Duration", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 6, str.tostring(ddDuration) + " bars", text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
// ========================================================================= //
// ============================= ALERTS ==================================== //
// ========================================================================= //
alertcondition(currentDd <= -critDd, title="Critical Drawdown Reached", message="Portfolio drawdown has fallen below the critical threshold!")
alertcondition(currentDd == 0 and currentDd[1] < 0, title="Full Recovery", message="Portfolio has fully recovered from the drawdown and hit a new high!")Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
Key Features
Required Recovery Metric: Mathematically tracks the asymmetric reality of drawdowns, showing exactly what percentage gain is required from the current trough to break even.
Dual Calculation Modes: Toggle between an All Time cumulative peak or a Rolling Window (useful for annualizing risk, e.g., standardizing to a 252-bar window).
Threshold Alerts: Includes alertcondition triggers for when the drawdown breaches your defined critical threshold, as well as a confirmation alert when the portfolio recovers to form a new peak.
Flexible Inputs: While it defaults to the chart's close, you can input custom portfolio equations (like AAPL+TSLA or a synthetic pair) directly into the indicator settings to track broader asset baskets.
Would you prefer to adapt this script to track an automated trading system's live strategy.equity instead of the chart's source price?
Required Recovery Metric: Mathematically tracks the asymmetric reality of drawdowns, showing exactly what percentage gain is required from the current trough to break even.
Dual Calculation Modes: Toggle between an All Time cumulative peak or a Rolling Window (useful for annualizing risk, e.g., standardizing to a 252-bar window).
Threshold Alerts: Includes alertcondition triggers for when the drawdown breaches your defined critical threshold, as well as a confirmation alert when the portfolio recovers to form a new peak.
Flexible Inputs: While it defaults to the chart's close, you can input custom portfolio equations (like AAPL+TSLA or a synthetic pair) directly into the indicator settings to track broader asset baskets.
Would you prefer to adapt this script to track an automated trading system's live strategy.equity instead of the chart's source price?
Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
The key shift here is replacing the price source with strategy.equity, which dynamically tracks both your closed balance and floating unrealized profit/loss. This provides a true real-time metric, which is critical for high-volume strategies where floating drawdowns can breach thresholds before a trade is even closed.
I've included a generic moving average crossover at the top just so the script generates trades and compiles out of the box. You can replace that top section with your own entry/exit logic.
I've included a generic moving average crossover at the top just so the script generates trades and compiles out of the box. You can replace that top section with your own entry/exit logic.
Code: Select all
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pavel Tuček | forex-scalping.com
//@version=5
strategy("Strategy Equity Drawdown Calculator", overlay=false, precision=2, initial_capital=10000)
// ========================================================================= //
// ==================== DUMMY STRATEGY (REPLACE THIS) ====================== //
// ========================================================================= //
// This section just generates trades so the equity curve has data to read.
fastMA = ta.sma(close, 10)
slowMA = ta.sma(close, 50)
if ta.crossover(fastMA, slowMA)
strategy.entry("Long", strategy.long)
if ta.crossunder(fastMA, slowMA)
strategy.entry("Short", strategy.short)
// ========================================================================= //
// ================== DRAWDOWN CALCULATOR MODULE =========================== //
// ========================================================================= //
grp_dd = "Drawdown Settings"
mode = input.string("All Time", title="Calculation Mode", options=["All Time", "Rolling Window"], group=grp_dd)
window = input.int(252, title="Rolling Window (Bars)", minval=1, tooltip="Used only if 'Rolling Window' is selected.", group=grp_dd)
critDd = input.float(20.0, title="Critical Drawdown Threshold (%)", step=1.0, group=grp_dd)
grp_ui = "UI & Visuals"
showTable = input.bool(true, title="Show Dashboard Panel", group=grp_ui)
tablePos = input.string(position.top_right, title="Dashboard Position", options=[position.top_right, position.top_left, position.bottom_right, position.bottom_left], group=grp_ui)
// Track live equity instead of price
float src = strategy.equity
var float allTimePeak = na
var float maxDdAllTime = 0.0
var int peakBarIndex = na
float currentPeak = na
// Determine Peak
if mode == "All Time"
if na(allTimePeak) or src >= allTimePeak
allTimePeak := src
peakBarIndex := bar_index
currentPeak := allTimePeak
else
currentPeak := ta.highest(src, window)
// Calculate Current Drawdown
float currentDd = currentPeak > 0 ? ((src - currentPeak) / currentPeak) * 100 : 0.0
// Calculate Max Drawdown
if mode == "All Time"
maxDdAllTime := math.min(maxDdAllTime, currentDd)
float maxDd = mode == "All Time" ? maxDdAllTime : ta.lowest(currentDd, window)
// Calculate Required Recovery %
float recoveryReq = currentPeak > 0 and src > 0 ? ((currentPeak / src) - 1.0) * 100.0 : 0.0
// Calculate Drawdown Duration (bars)
int ddDuration = mode == "All Time" ? (bar_index - peakBarIndex) : int(math.abs(ta.highestbars(src, window)))
// ========================================================================= //
// ============================= PLOTTING ================================== //
// ========================================================================= //
// Dynamic color for the drawdown area
color ddColor = currentDd <= -critDd ? color.new(color.red, 50) : color.new(color.orange, 50)
plot(currentDd, title="Current Drawdown", color=ddColor, style=plot.style_area, linewidth=1)
plot(maxDd, title="Max Drawdown", color=color.red, style=plot.style_line, linewidth=2)
hline(0, title="Zero Line", color=color.gray, linestyle=hline.style_dashed)
hline(-critDd, title="Critical Level", color=color.new(color.red, 30), linestyle=hline.style_dotted)
// ========================================================================= //
// ======================== DASHBOARD TABLE ================================ //
// ========================================================================= //
if showTable
var table dash = table.new(tablePos, 2, 7, border_width=1, border_color=color.new(color.gray, 80), frame_color=color.new(color.gray, 80), frame_width=1)
if barstate.islast
color bgHead = color.new(color.black, 20)
color bgCell = color.new(color.black, 50)
color textVal = color.white
color alertText = currentDd <= -critDd ? color.red : color.white
table.cell(dash, 0, 0, "Metric", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_left)
table.cell(dash, 1, 0, "Value", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_right)
table.cell(dash, 0, 1, "Peak Equity", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 1, str.tostring(currentPeak, "#.##"), text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 2, "Current Equity", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 2, str.tostring(src, "#.##"), text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 3, "Current Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 3, str.tostring(currentDd, "#.##") + " %", text_color=alertText, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 4, "Required Recovery", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 4, str.tostring(recoveryReq, "#.##") + " %", text_color=color.yellow, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 5, "Max Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 5, str.tostring(maxDd, "#.##") + " %", text_color=color.red, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 6, "Drawdown Duration", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 6, str.tostring(ddDuration) + " bars", text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
// ========================================================================= //
// ============================= ALERTS ==================================== //
// ========================================================================= //
// Strategies use the dynamic alert() function rather than alertcondition()
if currentDd <= -critDd and currentDd[1] > -critDd
alert("Critical Equity Drawdown Reached! Current DD: " + str.tostring(currentDd, "#.##") + "%", alert.freq_once_per_bar_close)
if currentDd == 0 and currentDd[1] < 0
alert("Strategy fully recovered. New equity high reached!", alert.freq_once_per_bar_close)Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
Strategy-Specific Changes
Floating Equity: strategy.equity updates dynamically on every tick (if recalculating on every tick is enabled) or at the bar close, fully reflecting open positions.
Alert Mechanics: In strategy() scripts, alertcondition() behaves inconsistently. The code now uses the alert() function nested in if statements, ensuring alerts fire exactly when thresholds are crossed.
Floating Equity: strategy.equity updates dynamically on every tick (if recalculating on every tick is enabled) or at the bar close, fully reflecting open positions.
Alert Mechanics: In strategy() scripts, alertcondition() behaves inconsistently. The code now uses the alert() function nested in if statements, ensuring alerts fire exactly when thresholds are crossed.
Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
Here is the complete script with the integrated kill-switch.
To ensure the kill-switch reacts to floating drawdowns instantly rather than waiting for the candle to close, I have added calc_on_every_tick=true to the strategy declaration. Once the critical drawdown is breached, the script cancels all pending orders, flattens any open positions, and permanently locks out new entries until you manually restart the script.
To ensure the kill-switch reacts to floating drawdowns instantly rather than waiting for the candle to close, I have added calc_on_every_tick=true to the strategy declaration. Once the critical drawdown is breached, the script cancels all pending orders, flattens any open positions, and permanently locks out new entries until you manually restart the script.
Code: Select all
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pavel Tuček | forex-scalping.com
//@version=5
strategy("Strategy Equity Drawdown Calculator", overlay=false, precision=2, initial_capital=10000, calc_on_every_tick=true)
// ========================================================================= //
// ================== DRAWDOWN & KILL-SWITCH SETTINGS ====================== //
// ========================================================================= //
grp_dd = "Drawdown Settings"
mode = input.string("All Time", title="Calculation Mode", options=["All Time", "Rolling Window"], group=grp_dd)
window = input.int(252, title="Rolling Window (Bars)", minval=1, tooltip="Used only if 'Rolling Window' is selected.", group=grp_dd)
critDd = input.float(20.0, title="Critical Drawdown Threshold (%)", step=1.0, group=grp_dd)
useKill = input.bool(true, title="Enable Hard Kill-Switch", tooltip="Flattens positions and halts trading if critical DD is reached.", group=grp_dd)
grp_ui = "UI & Visuals"
showTable = input.bool(true, title="Show Dashboard Panel", group=grp_ui)
tablePos = input.string(position.top_right, title="Dashboard Position", options=[position.top_right, position.top_left, position.bottom_right, position.bottom_left], group=grp_ui)
// ========================================================================= //
// ================== DRAWDOWN CALCULATOR MODULE =========================== //
// ========================================================================= //
// Track live equity
float src = strategy.equity
var float allTimePeak = na
var float maxDdAllTime = 0.0
var int peakBarIndex = na
float currentPeak = na
// Determine Peak
if mode == "All Time"
if na(allTimePeak) or src >= allTimePeak
allTimePeak := src
peakBarIndex := bar_index
currentPeak := allTimePeak
else
currentPeak := ta.highest(src, window)
// Calculate Current Drawdown
float currentDd = currentPeak > 0 ? ((src - currentPeak) / currentPeak) * 100 : 0.0
// Calculate Max Drawdown
if mode == "All Time"
maxDdAllTime := math.min(maxDdAllTime, currentDd)
float maxDd = mode == "All Time" ? maxDdAllTime : ta.lowest(currentDd, window)
// Calculate Required Recovery %
float recoveryReq = currentPeak > 0 and src > 0 ? ((currentPeak / src) - 1.0) * 100.0 : 0.0
// Calculate Drawdown Duration (bars)
int ddDuration = mode == "All Time" ? (bar_index - peakBarIndex) : int(math.abs(ta.highestbars(src, window)))
// ========================================================================= //
// ======================= KILL-SWITCH LOGIC =============================== //
// ========================================================================= //
// Persistent state for the kill-switch
var bool isKilled = false
if useKill and currentDd <= -critDd
isKilled := true
// Trigger flatten on the exact tick the switch flips
if isKilled and not isKilled[1]
strategy.cancel_all() // Remove any pending limit/stop orders
strategy.close_all(comment="KILL SWITCH TRIPPED")
alert("KILL SWITCH ACTIVATED: All positions closed due to critical drawdown breach.", alert.freq_once_per_bar_close)
// ========================================================================= //
// ==================== DUMMY STRATEGY (REPLACE THIS) ====================== //
// ========================================================================= //
fastMA = ta.sma(close, 10)
slowMA = ta.sma(close, 50)
// Entry logic is wrapped in "not isKilled" to prevent new trades
if not isKilled
if ta.crossover(fastMA, slowMA)
strategy.entry("Long", strategy.long)
if ta.crossunder(fastMA, slowMA)
strategy.entry("Short", strategy.short)
// ========================================================================= //
// ============================= PLOTTING ================================== //
// ========================================================================= //
color ddColor = currentDd <= -critDd ? color.new(color.red, 50) : color.new(color.orange, 50)
plot(currentDd, title="Current Drawdown", color=ddColor, style=plot.style_area, linewidth=1)
plot(maxDd, title="Max Drawdown", color=color.red, style=plot.style_line, linewidth=2)
hline(0, title="Zero Line", color=color.gray, linestyle=hline.style_dashed)
hline(-critDd, title="Critical Level", color=color.new(color.red, 30), linestyle=hline.style_dotted)
// ========================================================================= //
// ======================== DASHBOARD TABLE ================================ //
// ========================================================================= //
if showTable
var table dash = table.new(tablePos, 2, 8, border_width=1, border_color=color.new(color.gray, 80), frame_color=color.new(color.gray, 80), frame_width=1)
if barstate.islast
color bgHead = color.new(color.black, 20)
color bgCell = color.new(color.black, 50)
color textVal = color.white
color alertText = currentDd <= -critDd ? color.red : color.white
table.cell(dash, 0, 0, "Metric", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_left)
table.cell(dash, 1, 0, "Value", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_right)
// Status Row (Shows RED if killed)
table.cell(dash, 0, 1, "System Status", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 1, isKilled ? "HALTED (KILLED)" : "ACTIVE", text_color=isKilled ? color.red : color.lime, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 2, "Peak Equity", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 2, str.tostring(currentPeak, "#.##"), text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 3, "Current Equity", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 3, str.tostring(src, "#.##"), text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 4, "Current Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 4, str.tostring(currentDd, "#.##") + " %", text_color=alertText, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 5, "Required Recovery", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 5, str.tostring(recoveryReq, "#.##") + " %", text_color=color.yellow, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 6, "Max Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 6, str.tostring(maxDd, "#.##") + " %", text_color=color.red, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 7, "Drawdown Duration", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 7, str.tostring(ddDuration) + " bars", text_color=textVal, bgcolor=bgCell, text_halign=text.align_right)
// ========================================================================= //
// ============================= ALERTS ==================================== //
// ========================================================================= //
if currentDd == 0 and currentDd[1] < 0 and not isKilled
alert("Strategy fully recovered. New equity high reached!", alert.freq_once_per_bar_close)Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
How the Kill-Switch Operates
Persistent State Lock: The script uses var bool isKilled. Once this boolean flips to true, it cannot revert to false automatically. It physically blocks all strategy.entry() functions from firing indefinitely until you manually reload the indicator or reset the chart.
Total Flattening: When triggered, strategy.cancel_all() pulls any resting limit or stop orders out of the market, and strategy.close_all() forcefully exits all open market positions instantly.
Dashboard Feedback: A new "System Status" row is added to the top of your UI table. It will cleanly show ACTIVE in green during normal operation, and snap to HALTED (KILLED) in red the moment the threshold is breached.
Persistent State Lock: The script uses var bool isKilled. Once this boolean flips to true, it cannot revert to false automatically. It physically blocks all strategy.entry() functions from firing indefinitely until you manually reload the indicator or reset the chart.
Total Flattening: When triggered, strategy.cancel_all() pulls any resting limit or stop orders out of the market, and strategy.close_all() forcefully exits all open market positions instantly.
Dashboard Feedback: A new "System Status" row is added to the top of your UI table. It will cleanly show ACTIVE in green during normal operation, and snap to HALTED (KILLED) in red the moment the threshold is breached.
Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
And for those, who want to learn how to make it even better i prepared this special version.
Here is the professional-grade implementation.
For a high-volume algorithmic trading environment, standard procedural Pine Script can become inefficient. This "Pro standard" version introduces User-Defined Types (UDTs) for state management, highly optimized UI rendering (preventing redrawing overhead on every tick), JSON webhook payload construction, and realistic execution parameters (slippage/commission) to ensure the equity curve reflects actual trading conditions.
Here is the professional-grade implementation.
For a high-volume algorithmic trading environment, standard procedural Pine Script can become inefficient. This "Pro standard" version introduces User-Defined Types (UDTs) for state management, highly optimized UI rendering (preventing redrawing overhead on every tick), JSON webhook payload construction, and realistic execution parameters (slippage/commission) to ensure the equity curve reflects actual trading conditions.
Code: Select all
// This source code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Pavel Tuček | forex-scalping.com
//@version=5
strategy("Pro Equity Drawdown & Risk Engine", overlay=false, precision=2, initial_capital=10000, calc_on_every_tick=true, commission_type=strategy.commission.cash_per_order, commission_value=2.0, slippage=1)
// ========================================================================= //
// ========================== ENUMS & INPUTS =============================== //
// ========================================================================= //
grp_risk = "Risk Engine Parameters"
i_mode = input.string("All Time", "Calculation Mode", options=["All Time", "Rolling Window"], group=grp_risk)
i_window = input.int(252, "Rolling Window (Bars)", minval=1, group=grp_risk)
i_critDd = input.float(20.0, "Critical Drawdown (%)", step=0.5, tooltip="Threshold at which the Kill-Switch engages.", group=grp_risk)
i_useKill = input.bool(true, "Enable Hard Kill-Switch", tooltip="Flattens all positions and halts execution until indicator is manually reloaded.", group=grp_risk)
i_useJson = input.bool(true, "Format Alert as JSON", tooltip="Outputs a JSON payload for webhook routing when Kill-Switch trips.", group=grp_risk)
grp_ui = "Dashboard & Visuals"
i_showDash = input.bool(true, "Show Risk Dashboard", group=grp_ui)
i_dashPos = input.string(position.top_right, "Dashboard Position", options=[position.top_right, position.top_left, position.bottom_right, position.bottom_left], group=grp_ui)
// ========================================================================= //
// ==================== USER-DEFINED TYPES (UDTs) ========================== //
// ========================================================================= //
// Object to encapsulate all risk metrics, maintaining strict state isolation.
type RiskProfile
float equity
float peak
float currentDD
float maxDD
float recoveryReq
int ddDuration
int peakBarIndex
bool isKilled
// ========================================================================= //
// ======================== STATE INITIALIZATION =========================== //
// ========================================================================= //
// Initialize the global state object
var RiskProfile profile = RiskProfile.new(
equity = strategy.equity,
peak = strategy.equity,
currentDD = 0.0,
maxDD = 0.0,
recoveryReq = 0.0,
ddDuration = 0,
peakBarIndex = bar_index,
isKilled = false
)
// Update live equity
profile.equity := strategy.equity
// ========================================================================= //
// ======================== CORE METRIC LOGIC ============================== //
// ========================================================================= //
// Compute Rolling Data (Must be calculated in global scope to track series history)
float rollPeak = ta.highest(profile.equity, i_window)
int rollBars = int(math.abs(ta.highestbars(profile.equity, i_window)))
if i_mode == "All Time"
if profile.equity >= profile.peak
profile.peak := profile.equity
profile.peakBarIndex := bar_index
else
profile.peak := rollPeak
// Current Drawdown
profile.currentDD := profile.peak > 0 ? ((profile.equity - profile.peak) / profile.peak) * 100 : 0.0
// Max Drawdown
if i_mode == "All Time"
profile.maxDD := math.min(profile.maxDD, profile.currentDD)
else
// Needs series variable for ta.lowest
float seriesDD = profile.currentDD
profile.maxDD := ta.lowest(seriesDD, i_window)
// Required Recovery %
profile.recoveryReq := (profile.peak > 0 and profile.equity > 0) ? ((profile.peak / profile.equity) - 1.0) * 100.0 : 0.0
// Drawdown Duration
profile.ddDuration := i_mode == "All Time" ? (bar_index - profile.peakBarIndex) : rollBars
// ========================================================================= //
// ========================= KILL-SWITCH ENGINE ============================ //
// ========================================================================= //
// Evaluate risk threshold
if i_useKill and profile.currentDD <= -i_critDd
profile.isKilled := true
// Execution: Fire once upon state change
if profile.isKilled and not profile.isKilled[1]
strategy.cancel_all()
strategy.close_all(comment="KILL_SWITCH_TRIPPED")
// Construct dynamic alert payload
string alertMsg = na
if i_useJson
// Use double braces {{ }} to escape JSON brackets in str.format
alertMsg := str.format('{{"event": "kill_switch", "symbol": "{0}", "drawdown_percent": {1}, "equity": {2}, "time": "{3}"}}',
syminfo.tickerid, str.tostring(profile.currentDD, "#.##"), str.tostring(profile.equity, "#.##"), str.tostring(time))
else
alertMsg := "KILL SWITCH ACTIVATED: Critical drawdown breach at " + str.tostring(profile.currentDD, "#.##") + "%"
alert(alertMsg, alert.freq_once_per_bar_close)
// ========================================================================= //
// ==================== DUMMY STRATEGY (REPLACE THIS) ====================== //
// ========================================================================= //
// Strategy logic is strictly gated by the boolean object property
if not profile.isKilled
fastMA = ta.sma(close, 10)
slowMA = ta.sma(close, 50)
if ta.crossover(fastMA, slowMA)
strategy.entry("Long", strategy.long)
if ta.crossunder(fastMA, slowMA)
strategy.entry("Short", strategy.short)
// ========================================================================= //
// ======================== VISUALS & DASHBOARD ============================ //
// ========================================================================= //
color ddColor = profile.currentDD <= -i_critDd ? color.new(color.red, 50) : color.new(color.orange, 50)
plot(profile.currentDD, "Current Drawdown", ddColor, style=plot.style_area, linewidth=1)
plot(profile.maxDD, "Max Drawdown", color.red, style=plot.style_line, linewidth=2)
hline(0, "Zero Line", color.gray, hline.style_dashed)
hline(-i_critDd, "Critical Level", color.new(color.red, 30), hline.style_dotted)
// Optimal UI Rendering: Initialize table structure only on the first bar to save CPU cycles.
var table dash = table.new(i_dashPos, 2, 8, border_width=1, border_color=color.new(color.gray, 80), frame_color=color.new(color.gray, 80), frame_width=1)
var color bgHead = color.new(color.black, 10)
var color bgCell = color.new(color.black, 40)
if i_showDash and barstate.islast
// Only update values on the last bar/tick.
// Cell styling is reapplied here since we are overwriting the cells.
// Headers
table.cell(dash, 0, 0, "RISK METRIC", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_left)
table.cell(dash, 1, 0, "LIVE VALUE", text_color=color.gray, bgcolor=bgHead, text_halign=text.align_right)
// Status
table.cell(dash, 0, 1, "System Status", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 1, profile.isKilled ? "HALTED" : "ACTIVE", text_color=profile.isKilled ? color.red : color.lime, bgcolor=bgCell, text_halign=text.align_right)
// Metrics
table.cell(dash, 0, 2, "Peak Equity", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 2, str.tostring(profile.peak, "#.##"), text_color=color.white, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 3, "Live Equity", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 3, str.tostring(profile.equity, "#.##"), text_color=color.white, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 4, "Current Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 4, str.tostring(profile.currentDD, "#.##") + " %", text_color=profile.currentDD <= -i_critDd ? color.red : color.white, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 5, "Required Recovery", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 5, str.tostring(profile.recoveryReq, "#.##") + " %", text_color=color.yellow, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 6, "Max Drawdown", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 6, str.tostring(profile.maxDD, "#.##") + " %", text_color=color.red, bgcolor=bgCell, text_halign=text.align_right)
table.cell(dash, 0, 7, "Drawdown Duration", text_color=color.silver, bgcolor=bgCell, text_halign=text.align_left)
table.cell(dash, 1, 7, str.tostring(profile.ddDuration) + " bars", text_color=color.white, bgcolor=bgCell, text_halign=text.align_right)
// Recovery Alert (Only fires if the system wasn't killed)
if profile.currentDD == 0 and profile.currentDD[1] < 0 and not profile.isKilled
alert(i_useJson ? str.format('{{"event": "recovery", "symbol": "{0}", "equity": {1}}}', syminfo.tickerid, str.tostring(profile.equity, "#.##")) : "Strategy fully recovered. New equity high reached!", alert.freq_once_per_bar_close)Re: Pro Equity Drawdown & Risk Engine (Pine Script v5) + JSON Webhooks
Pro Level Architecture Changes
Object-Oriented Data Structure (UDTs): The script now uses a type RiskProfile custom object. By binding the tracking parameters (peak, current DD, kill-status) to an instance of profile, you completely isolate the risk engine's state from your entry/exit strategy logic.
JSON Webhook Payload Builder: Standard text alerts are difficult for third-party servers to parse. The kill-switch now generates strict JSON using str.format() (escaping brackets via {{ }}). You can route this directly into external APIs, MT4/MT5 bridges, or Discord bots.
Execution Realism: Added commission_type, commission_value, and slippage directly to the strategy() declaration. For high-volume scalping, tracking equity without accounting for the bid/ask spread and broker commissions creates a false peak.
Optimized UI Draw Loop: Standard Pine scripts define table.cell() statically on every tick, which causes massive CPU overhead in real-time execution. The UI now securely restricts metric drawing by leveraging var initializers combined with barstate.islast, ensuring table calculations only consume resources on the live tick, not historical processing.
Object-Oriented Data Structure (UDTs): The script now uses a type RiskProfile custom object. By binding the tracking parameters (peak, current DD, kill-status) to an instance of profile, you completely isolate the risk engine's state from your entry/exit strategy logic.
JSON Webhook Payload Builder: Standard text alerts are difficult for third-party servers to parse. The kill-switch now generates strict JSON using str.format() (escaping brackets via {{ }}). You can route this directly into external APIs, MT4/MT5 bridges, or Discord bots.
Execution Realism: Added commission_type, commission_value, and slippage directly to the strategy() declaration. For high-volume scalping, tracking equity without accounting for the bid/ask spread and broker commissions creates a false peak.
Optimized UI Draw Loop: Standard Pine scripts define table.cell() statically on every tick, which causes massive CPU overhead in real-time execution. The UI now securely restricts metric drawing by leveraging var initializers combined with barstate.islast, ensuring table calculations only consume resources on the live tick, not historical processing.