And for those, who want to learn how to make it even better i prepared this special version.
For a high-volume algorithmic trading environment, standard procedural Pine Script can become inefficient. This
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)