
The Volatility Cone: A Quant's Tool for Mapping Price Uncertainty
How Jennifer Aniston’s LolaVie brand grew sales 40% with CTV ads
The DTC beauty category is crowded. To break through, Jennifer Aniston’s brand LolaVie, worked with Roku Ads Manager to easily set up, test, and optimize CTV ad creatives. The campaign helped drive a big lift in sales and customer growth, helping LolaVie break through in the crowded beauty category.
② One strategy in this book returned 2.3× the S&P 500 on a risk-adjusted basis over 5 years.
Fully coded in Python. Yours to run today.
The 2026 Playbook — 30+ backtested strategies,
full code included, ready to deploy.
20% off until Tuesday. Use APRIL2026 at checkout.
$79 → $63.20 · Expires April 14.
→ Grab it before Tuesday
⑤ Most quant courses teach you to watch. This one makes you build.
Live. Weekly. With feedback on your actual code.
The AlgoEdge Quant Finance Bootcamp — 12 weeks of stochastic models, Black-Scholes, Heston, volatility surfaces, and exotic options. Built from scratch in Python.
Not pre-recorded. Not self-paced. Live sessions, weekly homework, direct feedback, and a full code library that's yours to keep.
Cohort size is limited intentionally — so every question gets answered.
→ Before you enroll, reach out for a 15-minute fit check. No pitch, no pressure.
📩 Email first: [email protected]
Premium Members – Your Full Notebook Is Ready
The complete Google Colab notebook from today’s article (with live data, full Hidden Markov Model, interactive charts, statistics, and one-click CSV export) is waiting for you.
Preview of what you’ll get:

Inside the SMA Strategy Lab
Data Loading — reads & plots 1-min BTC-USD OHLC from Bitstamp (2012–2025) with an interactive candlestick + volume chart
Data Preparation — intraday log returns, overnight gap handling, rolling 60-min volatility (no lookahead bias)
Jump Detection — flags moves exceeding k×σ, plots price with up/down jump markers and returns vs threshold bands
Strategy Logic — mean-reversion positions (short after jump up, long after jump down), forward-filled until next signal
Performance Metrics — Sharpe ratio, max drawdown, total return, win rate, trade count
Visualizations — equity curve, drawdown chart, strategy vs buy & hold, daily P&L bars, hourly jump overlay
Parameter Sensitivity — Sharpe/drawdown/signal count across k = {2, 3, 4, 5, 6, 7, 8, 10, 12}
Subsample Stability — performance breakdown across 2012–2017, 2018–2021, 2022–2025
Free readers – you already got the full breakdown and visuals in the article. Paid members – you get the actual tool.
Not upgraded yet? Fix that in 10 seconds here👇
Daily algorithmic trading signals, market insights, and code-ready strategies to sharpen your edge.
Google Collab Notebook With Full Code Is Available In the End Of The Article Behind The Paywall 👇 (For Paid Subs Only)
The goal of this post is to implement and backtest a high-frequency trading (HFT) strategy using historical 1-min BTC-USD OHLC data from Bitstamp [1].
Cryptocurrencies are known for their high volatility, meaning their prices can move up or down rapidly by significant amounts. This volatility creates opportunities for traders to buy at favorable prices and sell a few hours later once the value increases by a certain percentage. By repeating this process consistently, it is possible to generate meaningful daily returns. Even during broader downtrends, short-term intraday movements often present high-margin opportunities that can still be exploited for profit [2].
Conversely, mean reversion refers to the tendency of an asset to move back toward its recent average or fair value following significant price swings.
In the context of BTC-USD intraday trading, large price jumps are often followed by partial reversals as traders take profits or cut losses, liquidity providers exploit price dislocations, and market orders in one direction become exhausted, causing a natural pullback.
The key point is that volatility amplifies mean reversion signals [3], as small price moves may be noise, while large moves relative to recent volatility are more likely to revert, allowing traders to focus on meaningful price jumps.
On short timescales, BTC-USD often experiences sharp deviations from short-term averages due to liquidity gaps and reactionary trading, creating temporary price fluctuations that tend to revert quickly and are less influenced by longer-term macro fundamentals, making intraday mean reversion [3] more predictable.
By combining jump detection with mean-reversion logic [3], a trader can enter positions after extreme moves, exploit temporary imbalances before the market corrects, limit exposure to broader trends, and systematically capture small, high-probability profits from BTC’s intraday volatility.
That’s why our focus today is on implementing and backtesting the intraday volatility Jump Mean-Reversion (JMR) trading strategy.
Let’s get started! 🚀
Contents
· Loading Input Data
· Data Preparation
· Strategy Implementation
· Strategy Performance Analysis
· Conclusions
· References
· Explore More
· Contacts 📬
· Disclaimer 📜
When Pressure Rises, Here’s Where Leaders Turn
Costs rise. Clients delay. Pressure builds.
The Survival Hub gives you practical ways to respond from cutting costs to tightening operations and staying on top of revenue.
Built to help you take control when things feel uncertain.
Loading Input Data
Reading historical BTC-USD 1-min OHLC candle data from Bitstamp [1]
import pandas as pd
import numpy as np
df = pd.read_csv(
"btcusd_bitstamp_1min_2012-2025.csv.gz",
compression="gzip"
)
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s")
df = df.sort_values("timestamp").reset_index(drop=True)
df.tail()
timestamp open high low close volume
6846595 2025-01-06 23:56:00 102231.0 102231.0 102227.0 102227.0 0.068900
6846596 2025-01-06 23:57:00 102230.0 102232.0 102230.0 102232.0 0.199451
6846597 2025-01-06 23:58:00 102262.0 102280.0 102260.0 102280.0 0.104410
6846598 2025-01-06 23:59:00 102280.0 102280.0 102280.0 102280.0 0.007554
6846599 2025-01-07 00:00:00 102278.0 102291.0 102263.0 102263.0 0.523107Examining the general dataset structure
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6846600 entries, 0 to 6846599
Data columns (total 6 columns):
# Column Dtype
--- ------ -----
0 timestamp datetime64[ns]
1 open float64
2 high float64
3 low float64
4 close float64
5 volume float64
dtypes: datetime64[ns](1), float64(5)
memory usage: 313.4 MBThe dataset spans from 1 January 2012 to 7 January 2025, contains 6,846,600 records (90.4 MB zipped, 313.4 MB unzipped), and is fully clean with no missing minutes, duplicates, or null values.
Plotting OHLC candlesticks (top panel) with volume underneath
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Select last ~20000 rows
sample = df.iloc[-20000:].copy()
# Ensure datetime
sample['timestamp'] = pd.to_datetime(sample['timestamp'])
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.03,
row_heights=[0.7, 0.3]
)
# --- Candlestick ---
fig.add_trace(
go.Candlestick(
x=sample['timestamp'],
open=sample['open'],
high=sample['high'],
low=sample['low'],
close=sample['close'],
name='BTC-USD'
),
row=1, col=1
)
# --- Volume ---
fig.add_trace(
go.Bar(
x=sample['timestamp'],
y=sample['volume'],
marker=dict(color='black'),
name='Volume',
opacity=1
),
row=2, col=1
)
# Layout
fig.update_layout(
title='BTC-USD Candlestick with Volume (1-Minute)',
xaxis_rangeslider_visible=False,
plot_bgcolor='white',
height=800
)
# --- X-axis grid lines ---
fig.update_xaxes(
showgrid=True, # enable grid
gridcolor='lightgray', # color of grid lines
gridwidth=1, # thickness
row=1, col=1
)
# --- Y-axis grid lines for candlestick ---
fig.update_yaxes(
showgrid=True,
gridcolor='lightgray',
gridwidth=1,
row=1, col=1
)
# --- Y-axis grid lines for volume ---
fig.update_yaxes(
showgrid=True,
gridcolor='lightgray',
gridwidth=1,
row=2, col=1
)
fig.show()
1-Min BTC-USD Candlestick with Volume: Last ~2 Weeks
Data Preparation
In this section, we prepare the input data for a Jump Mean-Reversion (JMR) strategy by computing intraday volatility to detect jumps, while correctly handling overnight gaps and avoiding lookahead bias.
Extracting date and identifying new trading days
df['date'] = df['timestamp'].dt.date #Extracts the calendar date from each timestamp
df['new_day'] = df['date'] != df['date'].shift(1) # flags the first minute of a new trading dayThis is critical because overnight returns (from yesterday’s close to today’s open) are treated differently from intraday returns. The flag “new_day” ensures that the volatility at any minute only uses past intraday returns, and the model does not “see” tomorrow’s price moves when calculating signals.
Calculating the intraday log return of BTC-USD for each 1-min bar
# Close-to-close log returns (intraday)
df['log_ret_intraday'] = np.log(df['close'] / df['close'].shift(1)) # Shifts the closing price down by 1 row, so each row now has the previous minute’s close.
#This is essential because return at time t = (price now / price previous).Log returns are additive over time, which makes them convenient for cumulative returns and volatility calculations.
Calculating the overnight log return for BTC-USD, i.e., the price change from the previous day’s close to today’s open
# Overnight returns: today open vs previous close
df['log_ret_overnight'] = np.log(df['open'] / df['close'].shift(1)) # the ratio of today’s open to yesterday’s close.
# df['open'] The opening price of the current candle (typically the first minute of the new trading day).
# df['close'].shift(1) The previous row’s close (usually the last minute of the previous day). This represents the last price available before the overnight period.Overnight returns capture gaps that happen outside regular intraday trading. Separating them is critical because these gaps can be large and would distort intraday volatility if included.
Replacing the first intraday return of each day with the overnight return
# Replace first intraday return of each day with overnight return
df.loc[df['new_day'], 'log_ret_intraday'] = df.loc[df['new_day'], 'log_ret_overnight']This ensures that your intraday return series accurately reflects the true price change, including overnight gaps, without mixing it into the rest of the intraday returns.
Your ads ran overnight. Nobody was watching. Except Viktor.
One brand built 30+ landing pages through Viktor without a single developer.
Each page mapped to a specific ad group. All deployed within hours. Viktor wrote the code and shipped every one from a Slack message.
That same team has Viktor monitoring ad accounts across the portfolio and posting performance briefs before the day starts. One colleague. Always on. Across every account.
Creating a unified return column that will be used for further analysis
# Final return series (used for jumps)
df['log_ret'] = df['log_ret_intraday']Preparing a return series used exclusively for intraday volatility calculation, excluding overnight jumps
Compute intraday-only volatility ---
# Exclude overnight bars for volatility calculation
df['log_ret_intraday_vol'] = df['log_ret_intraday'].copy()
df.loc[df['new_day'], 'log_ret_intraday_vol'] = np.nan # remove overnight returnsThe reason is that overnight gaps can be very large, and if included in rolling volatility, they would inflate intraday volatility estimates. By removing overnight returns, we can get a clean measure of “normal” intraday variability, which is essential for detecting extreme moves or jumps.
Calculating rolling intraday volatility over the past 60 min for each min in our BTC-USD 1-min data
window = 60 # rolling window in minutes
df['vol'] = df['log_ret_intraday_vol'].rolling(window, min_periods=1).std()
#This will be used to detect “jumps”—price changes that are unusually large compared to recent volatility.By using a rolling window, the volatility adapts to changing market conditions.
Strategy Implementation
Let’s get to the core of our jump detection logic to identify unusually large price movements relative to recent volatility.
Defining the threshold value
k = 7 # is a multiplier that controls how extreme a move must be to be considered a “jump.”Here, a jump means a move larger than 7× recent volatility. A smaller k (e.g., 2–3) produces frequent but noisy signals, whereas a larger k (e.g., 5-7) identifies only rare, more significant jumps with cleaner signals.
Next, we flag a jump whenever the absolute return at a given minute exceeds k times the recent rolling volatility, identifying unusually large price moves relative to normal market behavior
df['jump'] = np.abs(df['log_ret']) > k * df['vol']
# --- 6. Inspect jumps ---
jumps = df[df['jump']]
print(f"Detected {len(jumps)} jumps")
print(jumps[['timestamp', 'open', 'close', 'log_ret', 'vol']].head())
Detected 4178 jumps
timestamp open close log_ret vol
627 2012-01-01 20:28:00 4.84 4.84 0.055216 0.007128
1064 2012-01-02 03:45:00 5.00 5.00 0.032523 0.004199
3284 2012-01-03 16:45:00 5.32 5.32 0.062035 0.008009
3441 2012-01-03 19:22:00 5.14 5.14 -0.034420 0.004444
3613 2012-01-03 22:14:00 5.14 5.14 -0.028765 0.003714This is bias-free because at time t we compare the current return with volatility computed only from past data, without using any future information.
Plotting the BTC-USD price with detected jumps
import matplotlib.pyplot as plt
# Select a smaller time window (very important for readability)
sample = df.iloc[-1000000:] # last 1000000 minutes
plt.figure(figsize=(12, 6))
plt.plot(sample['timestamp'], sample['close'], label='BTC Price')
# Overlay jumps
jump_points = sample[sample['jump']]
plt.scatter(jump_points['timestamp'], jump_points['close'], marker='o', label='Jumps',color='r')
plt.title("BTC Price with Detected Jumps")
plt.legend()
plt.xticks(rotation=45)
#plt.grid()
plt.show()
BTC-USD price with detected jumps
Plotting Returns vs Volatility Threshold
plt.figure(figsize=(16, 6))
plt.plot(sample['timestamp'], sample['log_ret'], label='Log Return')
plt.plot(sample['timestamp'], k * sample['vol'], linestyle='--', label='Threshold (+k·vol)',alpha=0.3)
plt.plot(sample['timestamp'], -k * sample['vol'], linestyle='--', label='Threshold (-k·vol)',alpha=0.3)
plt.title("Returns vs Volatility Threshold")
plt.legend()
plt.xticks(rotation=45)
plt.show()
BTC-USD Returns vs Volatility Threshold
Visualizing the Distribution of Log Returns
plt.figure(figsize=(8, 5))
plt.hist(df['log_ret'].dropna(), bins=1500)
plt.title("Distribution of Log Returns")
plt.xlim([-0.01,0.01])
plt.show()
Distribution of BTC-USD Log Returns
Examining the Rolling Intraday Volatility
plt.figure(figsize=(12, 5))
plt.plot(sample['timestamp'], sample['vol'])
plt.title("Rolling Intraday Volatility")
plt.xticks(rotation=45)
plt.show()
BTC-USD Rolling Intraday Volatility
Calculating and plotting the jump direction (up/down)
df['jump_up'] = df['log_ret'] > k * df['vol']
df['jump_down'] = df['log_ret'] < -k * df['vol']
sample = df.iloc[-1000000:]
plt.figure(figsize=(12, 6))
plt.plot(sample['timestamp'], sample['close'], label='Price')
plt.scatter(sample[sample['jump_up']]['timestamp'],
sample[sample['jump_up']]['close'],
label='Jump Up',color="cyan",s=50)
plt.scatter(sample[sample['jump_down']]['timestamp'],
sample[sample['jump_down']]['close'],
label='Jump Down',color="red",s=50)
plt.legend()
plt.title("Jump Direction Visualization")
plt.xticks(rotation=45)
plt.show()
BTC-USD Jump Direction Visualization
Creating positions (trading signals)
# Positions: mean-reversion
df['position'] = 0
df.loc[df['jump_up'], 'position'] = -1 # short
df.loc[df['jump_down'], 'position'] = 1 # long
# Forward-fill positions (hold until next signal)
df['position'] = df['position'].replace(0, np.nan).ffill().fillna(0) #hold that position until a new signal appears
#Without this, you would only trade for 1 minute per signal.
df['strategy_ret'] = df['position'].shift(1) * df['log_ret']
df['cum_pnl'] = df['strategy_ret'].cumsum()
df['cum_pnl_exp'] = np.exp(df['cum_pnl']) # convert log returns to equityThe strategy stays flat by default, goes short after upward jumps expecting a reversal, and goes long after downward jumps expecting a bounce, thereby implementing a mean-reversion approach. Using shift(1) ensures trades are executed at t+1 based on signals from time t, preventing trading on the same bar and avoiding lookahead bias.
Creating the equity growth curve
import matplotlib.pyplot as plt
# Use a subset (important!)
sample = df.iloc[-20000:] # last ~2 weeks of minutes
plt.figure(figsize=(12, 6))
plt.plot(sample['timestamp'], sample['cum_pnl_exp'], label='Strategy Equity')
plt.title("PnL Curve - Jump Mean-Reversion Strategy")
plt.xlabel("Time")
plt.ylabel("Equity (Growth of $1)")
plt.legend()
plt.xticks(rotation=45)
plt.show()
BTC-USD PnL Curve — Jump Mean-Reversion Strategy
Comparing the strategy to Buy & Hold
df['log_ret_clean'] = np.log(df['close'] / df['close'].shift(1))
df['buy_hold'] = np.exp(df['log_ret_clean'].cumsum())
sample = df.iloc[-20000:]
plt.figure(figsize=(12, 6))
#plt.plot(sample['timestamp'], sample['cum_pnl_exp'], label='Strategy')
plt.plot(sample['timestamp'], sample['buy_hold'], label='Buy & Hold')
#plt.title("Buy & Hold")
plt.legend()
plt.xticks(rotation=45)
plt.show()
BTC-USD Buy & Hold
Charting cumulative returns vs upward/downward jump signals
df['strategy_ret'] = df['position'].shift(1) * df['log_ret'] # assume trade at next minute
df['cum_strategy'] = df['strategy_ret'].cumsum().apply(np.exp)
df['cum_btc'] = df['log_ret'].cumsum().apply(np.exp)
plt.figure(figsize=(14,6))
plt.scatter(df.loc[df['jump_up'], 'timestamp'], df.loc[df['jump_up'], 'cum_btc'],
color='red', s=20, label='Jump Up')
plt.scatter(df.loc[df['jump_down'], 'timestamp'], df.loc[df['jump_down'], 'cum_btc'],
color='orange', s=20, label='Jump Down')
plt.xlabel('Time')
plt.ylabel('Cumulative Return')
plt.legend()
plt.grid()
plt.show()
BTC-USD Cumulative Returns vs Upward/Downward Jump Signals
Depicting the Cumulative Strategy Returns
df['date'] = df['timestamp'].dt.date
daily = df.groupby('date').agg({
'log_ret': 'sum',
'strategy_ret': 'sum'
}).reset_index()
daily['cum_btc'] = daily['log_ret'].cumsum().apply(np.exp)
daily['cum_strategy'] = daily['strategy_ret'].cumsum().apply(np.exp)
df['ret'] = np.exp(df['log_ret']) - 1 # per-minute pct return
df['strategy_ret_pct'] = df['position'].shift(1) * df['ret']
df['cum_strategy_pct'] = (1 + df['strategy_ret_pct']).cumprod()
df['cum_btc_pct'] = (1 + df['ret']).cumprod()
df['cum_strategy_pct'].plot()
plt.grid()
plt.title("Cumulative Strategy Returns")
plt.xlabel("Time Index")
plt.show()
BTC-USD Cumulative Strategy Returns
Graphing BTC Log Returns with Detected Jumps
df_plot = df.resample('1h', on='timestamp').agg({
'log_ret': 'mean',
'vol': 'mean',
'jump_up': 'max',
'jump_down': 'max'
}).reset_index()
plt.figure(figsize=(16,6))
# Log returns line
plt.plot(df_plot['timestamp'], df_plot['log_ret'], color='blue', alpha=0.6, label='Log Returns')
# Highlight jumps with sized markers proportional to magnitude
plt.scatter(df_plot.loc[df_plot['jump_up'], 'timestamp'],
df_plot.loc[df_plot['jump_up'], 'log_ret'],
color='green', s=np.abs(df_plot.loc[df_plot['jump_up'], 'log_ret'])*1e5,
label='Jump Up', alpha=0.6)
plt.scatter(df_plot.loc[df_plot['jump_down'], 'timestamp'],
df_plot.loc[df_plot['jump_down'], 'log_ret'],
color='red', s=np.abs(df_plot.loc[df_plot['jump_down'], 'log_ret'])*1e5,
label='Jump Down', alpha=0.4)
plt.title('BTC Log Returns with Detected Jumps')
plt.xlabel('Time')
plt.ylabel('Log Return')
plt.legend()
plt.grid(True)
plt.show()
BTC Log Returns with Detected Jumps
Strategy Performance Analysis
Calculating the strategy trades [1 0]
df['trade'] = (df['position'] != df['position'].shift(1)).astype(int)A trade is recorded as 1 when entering or changing positions (including flipping long to short), and 0 when holding the same position. It identifies exactly when trades occur, allowing you to count trades, apply transaction costs only at execution, and analyze overall strategy activity.
Counting the number of trades
num_trades = df['trade'].sum()
print("Number of Trades:", num_trades)
Number of Trades: 8302Comparing win rates of the strategy vs the percentage of positive 1-min returns
# Strategy Win Rate
active = df['position'].shift(1) != 0
wins = (df['strategy_ret_pct'] > 0) & active
win_rate = wins.sum() / active.sum()
print("Win Rate (only trades):", win_rate)
Win Rate (only trades): 0.09954534577650155
# The percentage of positive 1-min returns
wins = df['log_ret'] > 0
win_rate = wins.sum() / wins.count()
print("Win Rate:", win_rate)
Win Rate: 0.365841731662431They differ because the strategy trades only during rare jump events rather than across all market minutes. The market statistic (36%) reflects all minutes, whereas the strategy shows a lower win rate (10%) because it trades rare extreme events and bets against short-term momentum, where reversals occur less often but can be more profitable.
Examining the annualized Sharpe ratio
# Annualization factor for 1-min data (~365*24*60)
ann_factor = np.sqrt(365 * 24 * 60)
sharpe = df['strategy_ret'].mean() / df['strategy_ret'].std() * ann_factor
print("Sharpe Ratio:", sharpe)
ann_factor = np.sqrt(365 * 24 * 60)
sharpe = df['strategy_ret'].mean() / df['strategy_ret'].std() * ann_factor
print("Sharpe Ratio:", sharpe)
Sharpe Ratio: 0.6286569806820148A Sharpe ratio of ~0.63 indicates that the strategy has weak risk-adjusted performance, meaning returns are relatively low compared to the volatility (risk) taken.
Calculating the equity curve and Max Drawdown
df['cum_pnl'] = df['strategy_ret'].cumsum()
df['equity_curve'] = np.exp(df['cum_pnl'])
equity = df['equity_curve']
rolling_max = equity.cummax()
drawdown = (equity - rolling_max) / rolling_max
max_dd = drawdown.min()
print("Max Drawdown:", max_dd)
Max Drawdown: -0.29636858191591525A maximum drawdown of -0.29 means the strategy experienced a peak-to-trough loss of 29%, indicating a moderate level of downside risk that may be uncomfortable depending on our risk tolerance.
Plotting the equity curve
import matplotlib.pyplot as plt
plt.figure(figsize=(12,5))
plt.plot(df['timestamp'], df['equity_curve'])
plt.title("Equity Curve")
plt.grid()
plt.xlabel("Time")
plt.show()
Strategy Equity Curve
Visualizing the drawdown
plt.figure(figsize=(12,4))
plt.plot(df['timestamp'], drawdown)
plt.title("Drawdown")
plt.grid()
plt.xlabel("Time")
plt.show()
Strategy Drawdown
Calculating the total return
total_return = df['cum_pnl'].iloc[-1] - 1
print("Total Return:", total_return)
Total Return: 16.719530169428527Here, we pick the final value of the equity curve at the end of the backtest. Since we started at 1, subtracting 1 gives net profit as a fraction of the starting capital expressed as a multiple of starting capital.
That’s a 1672% gain over the entire backtest period 2012–2025 in our data. However, this is before accounting for transaction costs, slippage, or realistic execution constraints [4].
Minute-level BTC backtests often show extremely high returns when ignoring costs and slippage, but including realistic constraints can significantly reduce net total return [4].
Conclusions
The proposed JMR strategy exploits short-term mean reversion after extreme volatility jumps, but its profitability is highly sensitive to parameter choice, market regime, and trading costs.
Here are the key takeaways:
Mean-reversion can occur after large price jumps, especially in short timeframes, but it is not guaranteed, as some jumps may start new trends instead of reversing.
Signal quality depends heavily on the threshold value k, because a small k generates many noisy trades that can lead to overtrading, while a large k produces fewer but cleaner signals, resulting in a more selective and reliable edge.
This strategy operates like a high-frequency, short-term approach, using minute-level data to take positions triggered by rare, extreme events, with typically short holding periods that make it highly sensitive to execution quality.
Too frequent trading, especially with smaller k, makes the strategy highly vulnerable to transaction costs, as even minor fees or slippage can quickly wipe out profits.
The strategy’s performance depends on market conditions, excelling in range-bound or noisy markets but struggling in strong trending markets where price jumps tend to continue rather than reverse.
Volatility normalization k * vol is crucial, as scaling k by market volatility makes the strategy adaptive to changing conditions and more robust than using fixed thresholds.
The strategy carries tail risk because large price jumps can continue against positions, so proper risk management such as stop losses and careful position sizing is essential to limit losses.
Backtest realism matters because even if our setup incorporates overnight gaps and avoids lookahead bias, ignoring transaction costs and slippage means real-world performance will almost always be worse than the backtest [4].
Future work will concentrate on essential reality checks:
If the strategy fails when transaction costs are between 0.01 and 0.05, it is not robust [4].
Parameter sensitivity can be tested by trying k values of 3, 5, 7, and 10, with consistent results expected to persist across all values.
Subsample stability can be tested across periods 2012–2018, 2018–2022, and 2022–2025, with strong performance expected to remain consistent throughout.
Beyond reality checks, it is valuable to incorporate stop-losses and holding periods to capture true scalping behavior, perform a parameter grid search for k and the window, and identify potential overfitting risks.
Subscribe to our premium content to read the rest.
Become a paying subscriber to get access to this post and other subscriber-only content.
Upgrade






