A backtest strategy for an Ethereum perpetual futures bot with two modes: a 30-minute signal confirmed on one hour, or a one-hour signal confirmed on four hours.
//@version=5
strategy("ETH Perp Trading Bot V9.7 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_mode = "=== Strategy Mode ==="
strat_mode = input.string("30m-1H", "Strategy Mode", options=["30m-1H", "1H-4H"], group=gp_mode, tooltip="Select the timeframe mode. 30m-1H: 30m signal with 1H confirmation (run on 30m chart); 1H-4H: 1H signal with 4H confirmation (run on 1H chart).")
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 higher 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 (R Ratio)", group=gp_risk, tooltip="Minimum guaranteed profit as a ratio of trade risk (R value) once stop loss enters lock phase (e.g. 1.0 means profit = 1 * Risk)")
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")
gp_filter = "=== Trend Strength Filter (Auto-Trading) ==="
use_adx = input.bool(true, "Enable ADX Filter", group=gp_filter, tooltip="Turn on to prevent the bot from trading in choppy, sideways markets.")
adx_len = input.int(14, "ADX Length", group=gp_filter)
adx_threshold = input.int(20, "ADX Threshold", group=gp_filter, tooltip="Typical values: 20-25. ADX must be above this to open new positions.")
adx_tf = input.timeframe("30", "ADX Timeframe", options=["30", "60", "240"], group=gp_filter, tooltip="Select ADX calculation timeframe: 30 (30m), 60 (1H), 240 (4H).")
// Timeframe check to ensure backtest is run on the correct chart resolution based on mode
if strat_mode == "30m-1H" and timeframe.period != "30"
runtime.error("For 30m-1H mode, the strategy must be run on the 30-minute (30m) chart resolution.")
if strat_mode == "1H-4H" and timeframe.period != "60"
runtime.error("For 1H-4H mode, the strategy must be run on the 1-hour (1H) 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
// ==========================================
// Signal Timeframe indicators (evaluated on current chart resolution: 30m for 30m-1H, 1H for 1H-4H)
[st_val_sig, st_dir_sig_raw] = ta.supertrend(st_mult, st_period)
// Map to python-style direction: 1 = green (bullish), -1 = red (bearish)
last_sig_dir = st_dir_sig_raw == -1 ? 1 : -1
last_sig_st = st_val_sig
// 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
// Helper function to calculate ADX/DMI
f_adx(len) =>
[diplus, diminus, adx_val] = ta.dmi(len, len)
adx_val
// Calculate ADX on the requested timeframe to filter micro-chop
adx_val = request.security(syminfo.tickerid, adx_tf, f_adx(adx_len), barmerge.gaps_off, barmerge.lookahead_off)
// Condition: Is the market trending enough?
adx_is_trending = not use_adx or (adx_val > adx_threshold)
// HTF Timeframe indicators function (evaluated on HTF to avoid lookahead bias)
f_htf_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 HTF bar values
[st_val, st_dir, dema_val, close]
// Dynamic HTF period resolution based on strategy mode
htf_period = (strat_mode == "30m-1H") ? "60" : "240"
// Request HTF indicators safely
[htf_st, htf_st_dir, htf_dema, htf_close] = request.security(syminfo.tickerid, htf_period, f_htf_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: HTF Track
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) =>
equity_val = nz(strategy.equity, 10000.0)
risk_amt = (risk_mode == "fixed") ? risk_fixed_amount : (equity_val * risk_percent)
buffer_usdt = risk_amt * lock_profit_buffer
position_eth = qty_val * face_value
if position_eth == 0
entry_val
else
if is_long_pos
entry_val + buffer_usdt / position_eth
else
entry_val - buffer_usdt / 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_sig_st
pos_stop_loss := last_sig_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_sig_st < survival_to_locked_price) : (last_sig_st > survival_to_locked_price)
if is_survival
pos_phase := 1
pos_stop_loss := tighten_sl(last_sig_st, pos_stop_loss, is_long_pos)
else
// Stage 3 Check: Switch Track (HTF) Phase
is_htf = is_long_pos ? (htf_st > locked_to_hourly_price) : (htf_st < locked_to_hourly_price)
if is_htf
pos_phase := 3
pos_stop_loss := tighten_sl(htf_st, pos_stop_loss, is_long_pos)
else
// Stage 2 Check: Lock Profit Phase
pos_phase := 2
candidate_stop = last_sig_st
if is_long_pos
if last_sig_st > locked_to_hourly_price
candidate_stop := locked_to_hourly_price
else
if last_sig_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(htf_st_dir == 1, false) and nz(htf_close > htf_dema, false) and nz(last_sig_dir == 1, false) and nz(adx_is_trending, false)
can_short = nz(can_trade, false) and nz(htf_st_dir == -1, false) and nz(htf_close < htf_dema, false) and nz(last_sig_dir == -1, false) and nz(adx_is_trending, 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 htf_st_dir == -1
exit_signal := true
exit_reason := (strat_mode == "30m-1H" ? "1H ST Red" : "4H ST Red")
else if (pos_phase == 1 or pos_phase == 2) and last_sig_dir == -1
exit_signal := true
exit_reason := (strat_mode == "30m-1H" ? "30m ST Red" : "1H ST Red")
else // Short position
if pos_phase == 3 and htf_st_dir == 1
exit_signal := true
exit_reason := (strat_mode == "30m-1H" ? "1H ST Green" : "4H ST Green")
else if (pos_phase == 1 or pos_phase == 2) and last_sig_dir == 1
exit_signal := true
exit_reason := (strat_mode == "30m-1H" ? "30m ST Green" : "1H ST Green")
// Order execution (includes reversal rules)
if is_flat
if can_long
entry_qty = calculate_qty(last_sig_st)
strategy.entry("Long", strategy.long, qty=entry_qty)
else if can_short
entry_qty = calculate_qty(last_sig_st)
strategy.entry("Short", strategy.short, qty=entry_qty)
else if is_long
if exit_signal
if can_short
reverse_qty = calculate_qty(last_sig_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_sig_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_sig_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_sig_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_sig, title="Signal SuperTrend", color=st_dir_sig_raw == -1 ? color.green : color.red, linewidth=1, style=plot.style_line)
plot(htf_dema, title="HTF DEMA", color=color.new(color.blue, 30), linewidth=2)
plot(htf_st, title="HTF SuperTrend", color=htf_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.7 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 ? (strat_mode == "30m-1H" ? "3. Hourly" : "3. 4H Track") : "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.7 Strategy Backtest combines a SuperTrend with a DEMA and an optional ADX filter that blocks new entries in choppy markets. Risk is sized from a fixed amount or a percentage of equity with a leverage setting, a lock-profit stage measured in risk multiples, a loss cooldown and an equity circuit breaker.
The inputs you can change from the gear icon on the chart. Defaults are the author's.
Setting
Type
Default
Variable
Strategy Mode
string
30m-1H
strat_mode
SuperTrend Period
int
10
st_period
SuperTrend Multiplier
float
3.0
st_mult
DEMA Period
int
200
dema_period
Risk Mode
string
percent
risk_mode
Fixed Risk Amount (USDT)
float
10.0
risk_fixed_amount
Percent Risk of Equity (%)
float
2.0
risk_percent
Leverage
float
10.0
leverage
Lock Profit Buffer (R Ratio)
float
1.0
lock_profit_buffer
Auto-detect Face Value
bool
true
auto_face_value
Manual Face Value (contracts multiplier)
float
0.01
manual_face_value
Enable Cooldown (3 Losses)
bool
true
enable_cooldown
Show all 19 settings
Setting
Type
Default
Variable
Cooldown Duration (Hours)
int
48
cooldown_hours
Enable Circuit Breaker
bool
true
enable_cb
Circuit Breaker Equity Threshold
float
350.0
cb_equity
Enable ADX Filter
bool
true
use_adx
ADX Length
int
14
adx_len
ADX Threshold
int
20
adx_threshold
ADX Timeframe
timeframe
30
adx_tf
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.
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
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
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
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
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
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