Strategy · Pine Script v5 · On the chart

ETH Perp Trading Bot V9.6 Strategy Backtest

by xunxuntj

A backtest strategy for an Ethereum perpetual futures bot that must run on the 30-minute chart.

View on GitHub

GitHub stars1on the repository
Pine versionv5runs as is
Lines of code345
Settings14inputs
LicenceMIT
Last update3 days ago26 Oct 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 strategy, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart. The trades and results appear in the Strategy Tester tab; adjust the settings with the gear icon.

Source Code

345 lines of Pine Script v5 from archive/v9.7_legacy/eth_perp_v9_6_backtest.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

eth_perp_v9_6_backtest.pine
//@version=5
strategy("ETH Perp Trading Bot V9.6 Strategy Backtest", overlay=true, initial_capital=10000, default_qty_type=strategy.cash, default_qty_value=100, commission_type=strategy.commission.percent, commission_value=0.04, margin_long=10, margin_short=10, max_boxes_count=500, max_labels_count=500, max_lines_count=500)

// ==========================================
// 1. INPUT PARAMETERS
// ==========================================

gp_ind = "=== Technical Indicators ==="
st_period = input.int(10, "SuperTrend Period", group=gp_ind, tooltip="Lookback period for ATR in SuperTrend")
st_mult = input.float(3.0, "SuperTrend Multiplier", step=0.1, group=gp_ind, tooltip="ATR multiplier for SuperTrend bands")
dema_period = input.int(200, "DEMA Period", group=gp_ind, tooltip="DEMA length on the 1-hour timeframe")

gp_risk = "=== Sizing & Risk ==="
risk_mode = input.string("percent", "Risk Mode", options=["fixed", "percent"], group=gp_risk, tooltip="fixed: Fixed risk amount in USDT; percent: Risk is % of total equity")
risk_fixed_amount = input.float(10.0, "Fixed Risk Amount (USDT)", group=gp_risk, tooltip="Amount of USDT risked per trade based on stop loss distance")
risk_percent = input.float(2.0, "Percent Risk of Equity (%)", minval=0.1, maxval=100.0, group=gp_risk, tooltip="Risk percentage of total equity per trade") / 100.0
leverage = input.float(10.0, "Leverage", group=gp_risk, tooltip="Leverage used to calculate margin requirement")
lock_profit_buffer = input.float(1.0, "Lock Profit Buffer (USDT)", group=gp_risk, tooltip="Minimum guaranteed profit in USDT once stop loss enters lock phase")

gp_asset = "=== Asset Details ==="
auto_face_value = input.bool(true, "Auto-detect Face Value", group=gp_asset, tooltip="Detect face value based on symbol base currency (e.g. BTC=0.0001, ETH=0.01, SOL=1.0)")
manual_face_value = input.float(0.01, "Manual Face Value (contracts multiplier)", group=gp_asset, tooltip="If auto-detect is disabled, this multiplier is used for position size calculation")

gp_ctrl = "=== Cooldown & Circuit Breaker ==="
enable_cooldown = input.bool(true, "Enable Cooldown (3 Losses)", group=gp_ctrl, tooltip="Enable 48 hours pause after 3 consecutive losses")
cooldown_hours = input.int(48, "Cooldown Duration (Hours)", group=gp_ctrl)
enable_cb = input.bool(true, "Enable Circuit Breaker", group=gp_ctrl, tooltip="Enable circuit breaker if equity falls below threshold")
cb_equity = input.float(350.0, "Circuit Breaker Equity Threshold", group=gp_ctrl, tooltip="Stop trading if equity is below this amount")

// Timeframe check to ensure backtest is run on the correct 30m resolution
if timeframe.period != "30"
    runtime.error("This strategy must be run on the 30-minute (30m) chart resolution.")

// ==========================================
// 2. AUTO-DETECT CONTRACT FACE VALUE
// ==========================================

get_default_face_value() =>
    syminfo.pointvalue

face_value = auto_face_value ? get_default_face_value() : manual_face_value

// ==========================================
// 3. MULTI-TIMEFRAME INDICATORS
// ==========================================

// 30m Timeframe indicators (evaluated on current chart resolution)
[st_val_30m, st_dir_30m_raw] = ta.supertrend(st_mult, st_period)
// Map to python-style direction: 1 = green (bullish), -1 = red (bearish)
last_30m_dir = st_dir_30m_raw == -1 ? 1 : -1
last_30m_st = st_val_30m

// Custom Double Exponential Moving Average (DEMA) function (ta.dema is not built-in in Pine Script v5)
dema(src, length) =>
    ema1 = ta.ema(src, length)
    ema2 = ta.ema(ema1, length)
    2 * ema1 - ema2

// 1H Timeframe indicators function (evaluated on 1H to avoid lookahead bias)
f_1h_indicators() =>
    [st_val, st_dir_raw] = ta.supertrend(st_mult, st_period)
    dema_val = dema(close, dema_period)
    st_dir = st_dir_raw == -1 ? 1 : -1
    // Use lookahead_off in request.security to retrieve the completed 1-hour bar values
    [st_val, st_dir, dema_val, close]

// Request 1H indicators safely
[h1_st, h1_st_dir, h1_dema, h1_close] = request.security(syminfo.tickerid, "60", f_1h_indicators(), barmerge.gaps_off, barmerge.lookahead_off)

// ==========================================
// 4. COOLDOWN & CIRCUIT BREAKER STATE
// ==========================================

var int consecutive_losses = 0
var int cooldown_end_time = 0
var int cb_end_time = 0
var int last_closed_trades_count = 0
var int last_loss_time = 0

// Reset consecutive losses and cooldown end time after cooldown period ends
if cooldown_end_time > 0 and time >= cooldown_end_time
    consecutive_losses := 0
    cooldown_end_time := 0
    last_loss_time := 0

// Scan for closed trades to count consecutive losses and trigger cooldown
if strategy.closedtrades > last_closed_trades_count
    for i = last_closed_trades_count to strategy.closedtrades - 1
        trade_pnl = strategy.closedtrades.profit(i)
        
        // If more than 48 hours have passed since the last loss, reset the counter
        if consecutive_losses > 0 and last_loss_time > 0 and (time - last_loss_time) > 48 * 60 * 60 * 1000
            consecutive_losses := 0
            
        if trade_pnl < 0
            consecutive_losses := consecutive_losses + 1
            last_loss_time := time
        else
            consecutive_losses := 0
            last_loss_time := 0
        
        if enable_cooldown and consecutive_losses >= 3
            cooldown_end_time := time + cooldown_hours * 60 * 60 * 1000
            
    last_closed_trades_count := strategy.closedtrades

equity_val = nz(strategy.equity, 10000.0)

// Circuit Breaker triggers if equity drops below cb_equity (7 days stop)
if enable_cb and equity_val <= cb_equity and nz(strategy.equity[1], 10000.0) > cb_equity
    cb_end_time := time + 7 * 24 * 60 * 60 * 1000

in_cooldown = enable_cooldown and (time < cooldown_end_time)
in_cb = enable_cb and (equity_val <= cb_equity or time < cb_end_time)
can_trade = not (na(in_cooldown) ? false : in_cooldown) and not (na(in_cb) ? false : in_cb)

// ==========================================
// 5. POSITION SIZING & PHASE STATE MACHINE
// ==========================================

var float pos_entry_price = na
var float pos_stop_loss = na
var float pos_initial_30m_st = na
var int pos_phase = 0 // 0: None, 1: Survival, 2: Locked, 3: Hourly
var float pos_qty = na

calculate_qty(stop_val) =>
    equity_val = nz(strategy.equity, 10000.0)
    sl_dist = math.abs(close - stop_val)
    risk_amt = (risk_mode == "fixed") ? risk_fixed_amount : (equity_val * risk_percent)
    calc_qty = sl_dist > 0 and not na(sl_dist) ? math.floor(risk_amt / (sl_dist * face_value)) : 0.0
    
    // Leverage safety cap: position value cannot exceed (equity * leverage)
    max_qty_leverage = math.floor((equity_val * leverage) / (close * face_value))
    final_qty = na(max_qty_leverage) ? calc_qty : na(calc_qty) ? max_qty_leverage : math.min(calc_qty, max_qty_leverage)
    
    math.max(1.0, na(final_qty) ? 1.0 : final_qty) // Minimum 1 contract

calculate_lock_threshold(entry_val, qty_val, is_long_pos) =>
    position_eth = qty_val * face_value
    if position_eth == 0
        entry_val
    else
        if is_long_pos
            entry_val + lock_profit_buffer / position_eth
        else
            entry_val - lock_profit_buffer / position_eth

tighten_sl(candidate, current, is_long_pos) =>
    if na(current) or current <= 0
        candidate
    else
        if is_long_pos
            math.max(candidate, current)
        else
            math.min(candidate, current)

// Track position state changes
pos_size = strategy.position_size
prev_pos_size = strategy.position_size[1]

is_new_long = (pos_size > 0 and prev_pos_size <= 0)
is_new_short = (pos_size < 0 and prev_pos_size >= 0)
is_active_pos = (pos_size != 0)

if is_new_long or is_new_short
    pos_entry_price := strategy.position_avg_price
    pos_initial_30m_st := last_30m_st
    pos_stop_loss := last_30m_st
    pos_phase := 1 // Survival
    pos_qty := math.abs(pos_size)
else if not is_active_pos
    pos_entry_price := na
    pos_initial_30m_st := na
    pos_stop_loss := na
    pos_phase := 0
    pos_qty := na

// Update stop loss trailing and stage transitions
if is_active_pos
    is_long_pos = pos_size > 0
    qty_val = pos_qty
    
    survival_to_locked_price = pos_entry_price
    locked_to_hourly_price = calculate_lock_threshold(pos_entry_price, qty_val, is_long_pos)
    
    // Stage 1 Check: Survival Phase
    is_survival = is_long_pos ? (last_30m_st < survival_to_locked_price) : (last_30m_st > survival_to_locked_price)
    
    if is_survival
        pos_phase := 1
        pos_stop_loss := tighten_sl(last_30m_st, pos_stop_loss, is_long_pos)
    else
        // Stage 3 Check: Switch Track (Hourly) Phase
        is_hourly = is_long_pos ? (h1_st > locked_to_hourly_price) : (h1_st < locked_to_hourly_price)
        if is_hourly
            pos_phase := 3
            pos_stop_loss := tighten_sl(h1_st, pos_stop_loss, is_long_pos)
        else
            // Stage 2 Check: Lock Profit Phase
            pos_phase := 2
            candidate_stop = last_30m_st
            if is_long_pos
                if last_30m_st > locked_to_hourly_price
                    candidate_stop := locked_to_hourly_price
            else
                if last_30m_st < locked_to_hourly_price
                    candidate_stop := locked_to_hourly_price
            pos_stop_loss := tighten_sl(candidate_stop, pos_stop_loss, is_long_pos)

// ==========================================
// 6. ENTRY & EXIT TRIGGER LOGIC
// ==========================================

// Entry Conditions
can_long = nz(can_trade, false) and nz(h1_st_dir == 1, false) and nz(h1_close > h1_dema, false) and nz(last_30m_dir == 1, false)
can_short = nz(can_trade, false) and nz(h1_st_dir == -1, false) and nz(h1_close < h1_dema, false) and nz(last_30m_dir == -1, false)

// Position States
is_flat = strategy.position_size == 0
is_long = strategy.position_size > 0
is_short = strategy.position_size < 0

// Exit Conditions
exit_signal = false
exit_reason = ""

if is_active_pos
    is_long_pos = pos_size > 0
    if is_long_pos
        if pos_phase == 3 and h1_st_dir == -1
            exit_signal := true
            exit_reason := "1H ST Red"
        else if (pos_phase == 1 or pos_phase == 2) and last_30m_dir == -1
            exit_signal := true
            exit_reason := "30m ST Red"
    else // Short position
        if pos_phase == 3 and h1_st_dir == 1
            exit_signal := true
            exit_reason := "1H ST Green"
        else if (pos_phase == 1 or pos_phase == 2) and last_30m_dir == 1
            exit_signal := true
            exit_reason := "30m ST Green"

// Order execution (includes reversal rules)
if is_flat
    if can_long
        entry_qty = calculate_qty(last_30m_st)
        strategy.entry("Long", strategy.long, qty=entry_qty)
    else if can_short
        entry_qty = calculate_qty(last_30m_st)
        strategy.entry("Short", strategy.short, qty=entry_qty)

else if is_long
    if exit_signal
        if can_short
            reverse_qty = calculate_qty(last_30m_st)
            strategy.entry("Short", strategy.short, qty=reverse_qty, comment="Reverse Short")
        else
            strategy.close("Long", comment=exit_reason)
    else if can_short // Direct reversal
        reverse_qty = calculate_qty(last_30m_st)
        strategy.entry("Short", strategy.short, qty=reverse_qty, comment="Reverse Short")

else if is_short
    if exit_signal
        if can_long
            reverse_qty = calculate_qty(last_30m_st)
            strategy.entry("Long", strategy.long, qty=reverse_qty, comment="Reverse Long")
        else
            strategy.close("Short", comment=exit_reason)
    else if can_long // Direct reversal
        reverse_qty = calculate_qty(last_30m_st)
        strategy.entry("Long", strategy.long, qty=reverse_qty, comment="Reverse Long")

// Trailing / Dynamic Stop Loss order execution
if is_active_pos
    if pos_size > 0
        strategy.exit("SL Long", "Long", stop=pos_stop_loss)
    else
        strategy.exit("SL Short", "Short", stop=pos_stop_loss)

// ==========================================
// 7. CHART VISUALIZATION & HUD PANEL
// ==========================================

// Plot Stop Loss and Entry Prices
plot_entry = plot(pos_entry_price, title="Entry Price", color=color.blue, style=plot.style_linebr, linewidth=1)
plot_sl = plot(pos_stop_loss, title="Stop Loss", color=color.red, style=plot.style_linebr, linewidth=2)

qty_val_plot = is_active_pos ? pos_qty : 0.0
locked_to_hourly_price_plot = is_active_pos ? calculate_lock_threshold(pos_entry_price, qty_val_plot, pos_size > 0) : na
plot_lock = plot(pos_phase >= 2 ? locked_to_hourly_price_plot : na, title="Lock Profit Threshold", color=color.orange, style=plot.style_linebr, linewidth=1)

fill(plot_entry, plot_sl, color=color.new(color.red, 95), title="Risk Band")

// Plot underlying indicators for visual checks
plot(st_val_30m, title="30m SuperTrend", color=st_dir_30m_raw == -1 ? color.green : color.red, linewidth=1, style=plot.style_line)
plot(h1_dema, title="1H DEMA 200", color=color.new(color.blue, 30), linewidth=2)
plot(h1_st, title="1H SuperTrend", color=h1_st_dir == 1 ? color.new(color.green, 30) : color.new(color.red, 30), linewidth=2)

// Paint background during cooldown / cb
bgcolor(in_cb ? color.new(color.red, 95) : in_cooldown ? color.new(color.orange, 95) : na, title="Trading Restriction Background")

// HUD Table overlay in top-right
var table hud = table.new(position.top_right, 2, 8, bgcolor=color.new(color.black, 30), border_color=color.gray, border_width=1)

if barstate.islast
    table.cell(hud, 0, 0, "V9.6 Metric", text_color=color.white, bgcolor=color.blue, text_size=size.small)
    table.cell(hud, 1, 0, "Current Value", text_color=color.white, bgcolor=color.blue, text_size=size.small)
    
    // Trading status
    status_text = in_cb ? "Circuit Breaker" : in_cooldown ? "Cooldown (48h)" : "Normal"
    status_color = in_cb or in_cooldown ? color.red : color.green
    table.cell(hud, 0, 1, "Trading Status", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 1, status_text, text_color=status_color, text_size=size.small)
    
    // Phase
    phase_text = pos_phase == 1 ? "1. Survival" : pos_phase == 2 ? "2. Locked" : pos_phase == 3 ? "3. Hourly" : "None"
    phase_color = pos_phase == 1 ? color.aqua : pos_phase == 2 ? color.yellow : pos_phase == 3 ? color.purple : color.gray
    table.cell(hud, 0, 2, "Position Phase", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 2, phase_text, text_color=phase_color, text_size=size.small)
    
    // Size
    pos_eth = is_active_pos ? (pos_qty * face_value) : 0.0
    table.cell(hud, 0, 3, "Position Size (" + syminfo.basecurrency + ")", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 3, str.tostring(pos_eth) + (pos_size > 0 ? " LONG" : pos_size < 0 ? " SHORT" : ""), text_color=color.white, text_size=size.small)
    
    // Stop loss
    sl_text = is_active_pos ? str.tostring(pos_stop_loss, "#.##") : "N/A"
    table.cell(hud, 0, 4, "Active Stop Loss", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 4, sl_text, text_color=color.white, text_size=size.small)
    
    // Lock profit threshold
    lock_text = is_active_pos ? str.tostring(locked_to_hourly_price_plot, "#.##") : "N/A"
    table.cell(hud, 0, 5, "Lock profit threshold", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 5, lock_text, text_color=color.white, text_size=size.small)
    
    // Consecutive losses
    table.cell(hud, 0, 6, "Consecutive Losses", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 6, str.tostring(consecutive_losses) + " / 3", text_color=consecutive_losses >= 3 ? color.red : color.white, text_size=size.small)
    
    // Face value
    table.cell(hud, 0, 7, "Face Value Unit", text_color=color.white, text_size=size.small)
    table.cell(hud, 1, 7, str.tostring(face_value), text_color=color.white, text_size=size.small)

Overview

ETH Perp Trading Bot V9.6 Strategy Backtest uses a SuperTrend on the chart timeframe together with a DEMA from a higher timeframe to set direction. Position size comes from either a fixed or percentage risk against the stop distance, with leverage and contract face value settings. A lock-profit stop phase, a cooldown after consecutive losses and an equity circuit breaker restrict trading.

Details

Published by

xunxuntj on GitHub

Type

Strategy, Pine Script v5

Draws

On the price chart

Category

Trend

Markets

Crypto

Timeframes

M15-H1

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Settings

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

SettingTypeDefaultVariable
SuperTrend Periodint10st_period
SuperTrend Multiplierfloat3.0st_mult
DEMA Periodint200dema_period
Risk Modestringpercentrisk_mode
Fixed Risk Amount (USDT)float10.0risk_fixed_amount
Percent Risk of Equity (%)float2.0risk_percent
Leveragefloat10.0leverage
Lock Profit Buffer (USDT)float1.0lock_profit_buffer
Auto-detect Face Valuebooltrueauto_face_value
Manual Face Value (contracts multiplier)float0.01manual_face_value
Enable Cooldown (3 Losses)booltrueenable_cooldown
Cooldown Duration (Hours)int48cooldown_hours
Show all 14 settings
SettingTypeDefaultVariable
Enable Circuit Breakerbooltrueenable_cb
Circuit Breaker Equity Thresholdfloat350.0cb_equity
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

Doge SMA

by hackingthemarkets
474 stars

Doge SMA is a long-only moving-average crossover strategy drawn on the price chart. It calculates a fast and a slow simple moving average of close and enters a long position when the fast average crosses above the slow average, but only inside a user-defined s

Strategy v4 TrendMoving averages
Updated 4 years ago · None stated GitHub repository of Doge SMA

This strategy calculates and plots a fast Exponential Moving Average (EMA) and a slow EMA directly on the main price chart. It enters long trades when the fast EMA crosses above the slow EMA and exits when the fast EMA crosses below the slow EMA. Inputs allow

Strategy v6 TrendMoving averages
Updated yesterday · AGPL-3.0 GitHub repository of Forven S01508 — Simple EMA20/50 Cross
330 stars

2 EMA/SMA + RSI Strategy [Alorse] combines three configurable moving averages with a short-period Relative Strength Index filter. It plots fast, slow, and trend moving averages on the price chart. Long entries trigger when the fast moving average is above the

Strategy v4 TrendOscillators & momentum
Updated 2 months ago · None stated GitHub repository of 2 EMA/SMA + RSI Strategy [Alorse]

Acrypto - Weighted Strategy

by AlbertoCuadra
318 stars

This strategy plots on the price chart and opens long or short positions when a weighted score from MACD, Stochastic RSI, RSI, Supertrend and a moving-average crossover exceeds a user-set trigger. Each component has its own weight and candle delay. Entries can

Strategy v5 TrendOscillators & momentum
Updated 3 years ago · Custom licence GitHub repository of Acrypto - Weighted Strategy
285 stars

Mean Reversion Strategy is an overlay strategy that identifies pullbacks using a long-term EMA, RSI, and candlestick patterns. It generates long signals when a bullish engulfing candle forms below the EMA while RSI is oversold, and short signals when a bearish

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

This strategy calculates dynamic support and resistance levels using pivot highs and lows alongside a custom smoothed trend filter and a 3-period EMA. It identifies entry opportunities when price breaks out relative to the trend filter and short-term EMA cross

Strategy v5 TrendSupport & resistance Repaints
Updated 6 weeks ago · None stated GitHub repository of 30s - ES 1m Strategy Candle Bar

More strategys

Trade Your Charts With a Regulated Broker

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

Compare brokers