Gain Lock In Backtests for Trading ETFs Based on Major Market Indexes
The Security Trading Analytics blog includes a growing
series of posts featuring the Proper Order Gain Lock‑in (POGL) model because of
its repeated success in different contexts.
The model implements an algorithm for specifying when to buy and sell a financial
security. This post presents backtested
results for the model’s gain lock-in feature.
- If you have ever been frustrated by watching accumulated gains evaporate before you can harvest them, this post may be worth your time.
- Data analysts may also find value in the discussion of the model, the demonstration of a backtesting workflow, and the evaluation of backtested outcomes.
- Listener‑friendly audio deep dives on the POGL model are available in either of the following posts:
An Overview of the POGL Model
The POGL model was initially introduced for specifying entry and exit trade dates for a single collection of tickers in the “A Preliminary Analysis of EMA Period Lengths and Price Action in a Buy-Sell Model” post. Two subsequent posts (A Model to Lock-in Gains When Trading Single-Stock ETFs and Should I Trade Single-Stock Leveraged ETFs?) illustrated how to adapt the POGL model for specifying entry and exit trade dates for a paired collection of tickers. A more intensive code-based post (as soon as a proper order exists between close, EMA_fast, and EMA_slowdifferent proper order definitions for unleveraged ETFs based on major market indexes versus 3X leveraged ETFs based on major market indexes.
Some key points for the POGL model include the following.
- The POGL Model assesses when to buy a security based on the commencement of a proper order relationship between specific Exponential Moving Average (EMA) values and their underlying closing prices (closing price > EMA_fast > EMA_slow). The model assumes that a proper order relationship can identify the beginning of timeframes when a ticker’s prices are generally rising.
- EMA_fast represents an EMA series value based on fewer, more recent trading days, and EMA_slow denotes an EMA series based on more, less recent trading days. As a result, EMA_fast responds more quickly to daily price changes than EMA_slow. Of course, the closing price at the end of a regular trading day is always the most recent price.
- The model logic dictates when to exit a position by attempting to lock in any accumulated gains before price pullbacks substantially diminish those gains or even cause a loss.
- At the time a security is bought, the POGL model sets an upper price boundary (above the entry price) and a lower price boundary (below the entry price). This post examines the impact of upper and lower parameter values on a ticker’s model performance.
- If prices continue to rise after a ticker’s security is bought, the closing price will eventually exceed the upper price boundary, prompting the model to dynamically reset both the upper and lower boundaries at higher values. This process repeats each time a closing price rises past the current upper price boundary.
- In this way, the model hitches a ride on a rising price trend for as long as an upward price trend exists.
- The model’s trading account balance is updated by selling a ticker’s security on the open price of the day after a close price falls below the lower price boundary.
- Beginning on the day after a trade exit, a position can be re-opened at the open price as soon as a proper order exists between close, EMA_fast, and EMA_slow.
- When using the model with a matched pair of tickers, entry and exit dates are derived for the base set of tickers, which would be the historical price series for the unleveraged ETFs. Then, the entry and exit dates for the base price series are applied to the other ticker set, which is the one for 3X leveraged ETFs in this post.
Tickers and Source Data for This Post
The source data for this post is a subset of the data from a prior post (Proper Order Definition Backtests for Trading ETFs Based on Major Market Indexes). The prior post ran and analyzed results from backtesting five alternative proper order definitions. The analysis permitted the comparison of the proper order definitions based on average return across trades specified by the model.
The current post demonstrates the implementation of backtests for five different sets of upper and lower price boundaries. While proper order definitions in combination with market action determine entry trade dates, upper and lower price boundaries in combination with market action determine exit trade dates. Consequently, this post and its predecessor complement each other by revealing the performance impacts of different sets of proper order definitions and different sets of lower and upper price boundaries.
The ETF tickers tracked in this post are for a set of five unleveraged ETFs based on major market indexes and five corresponding 3X leveraged ETFs. The following two tables show the tickers with their ETFs for each set.
The next two sets of bullet points highlight exactly what
each member of both ticker sets track.
- Underlying Market Indexes
- DIA: Tracks the Dow Jones Industrial Average
- SPY: Tracks the S&P 500 Index
- QQQ: Tracks the Nasdaq-100 Index
- SOXX: Tracks the NYSE Semiconductor Index
- IWM: Tracks the Russell 2000 Index
- 3X Leveraged ETFs
- UDOW: Tracks 3X DIA
- SPXL: Tracks 3X SPY
- TQQQ: Tracks 3X QQQ
- SOXL: Tracks 3X SOXX
- TNA: Tracks 3X IWM
The source data from the prior post is stored within the stockhistory_with_emas table. Within my development environment, this table resides in the securitytradinganalytics SQL Server database.
The following script shows how to return the first and last source data dates for which both the unleveraged and leveraged tickers had historical price data. The script uses two common table expressions (CTEs). The first CTE (ticker_bounds) pulls minimum and maximum dates by ticker from the stockhistory_with_emas table. The second CTE (matched_pairs) matches each unleveraged ticker with its corresponding leveraged ticker. The trailing select statement joins the result sets from the two CTEs to return the first and last date for each matched ticker pair.
A non-standard ticker was used for the DIA ticker. The code designates data for the DIA ticker as DIA_GS. This is because of a discovered and remedied DIA data anomaly with the source data from one of the two source data providers for this parametric study. The DIA_GS ticker in this post contains valid data for the DIA ticker. More detail on this issue is available in the predecessor post to this one.
use securitytradinganalytics;
go
with ticker_bounds as (
select
ticker,
min(date)
as first_date,
max(date)
as last_date
from
dbo.stockhistory_with_emas
where ticker in
(
'DIA_GS',
'SPY', 'QQQ', 'SOXX', 'IWM',
'UDOW',
'SPXL', 'TQQQ', 'SOXL', 'TNA'
)
and [close] is
not null
group by ticker
),
matched_pairs as (,
select * from
(values
('DIA_GS',
'UDOW'),
('SPY', 'SPXL'),
('QQQ', 'TQQQ'),
('SOXX', 'SOXL'),
('IWM', 'TNA')
) as
pairs(unleveraged_ticker, leveraged_ticker)
)
select
p.unleveraged_ticker,
p.leveraged_ticker,
l.first_date as
pair_first_date,
l.last_date as pair_last_date
from matched_pairs p
inner join ticker_bounds b on p.unleveraged_ticker =
b.ticker
inner join ticker_bounds l on p.leveraged_ticker =
l.ticker;
The following screenshot shows the result set from the preceding script. As you can see from the pair_last_date column values, all collected data for this post ends as of May 29, 2026. In contrast, the pair_first_date column values are not identical. This is because ETFs were initially offered for sale to the public on different dates.
The model backtesting script and summary code for input to the next two sections are in the Code Appendix at the end of this post. A short architectural overview precedes the source code. The summary code result sets are copied into and formatted in an Excel worksheet. These actions help to highlight key findings.
Average Portfolio Return Percentage by Lower and Upper Price Boundaries
The following two tables present a top-level summary of overall and per trade average portfolio return percentage change.
There are two portfolios tracked in this section. The first portfolio is comprised of these
five unleveraged ETFs: DIA_GS, IWM, SOXX, SPY, and QQQ. The second portfolio is comprised of these
five leveraged ETFs: UDOW, TNA, SOXL, SPXL, and TQQQ.
The first table focuses on outcomes for five different
boundary settings.
- The Boundary Settings column itemizes the five pairs of lower and upper boundary settings tracked in this post. The bottom row specifies an initial lower boundary of 2.0 percent below the buy price and an upper boundary of 4.0 percent above the buy price. The largest difference in lower and upper boundary percentages is, respectively, -12.5 below and 25 percent above the entry price.
- The values in the grand total trades column show the total number of trades specified by the POGL model for each pair of lower and upper boundaries. The total number of trades decreases as the spread from the lower to the upper boundary values increases. This pattern is particularly apparent for the narrowest boundary spread for which the model specified 508 trades versus the widest boundary spread for which the model specified just 22 trades.
- The values in the avg unleveraged portfolio return percent column reveal the average return percent across all five unleveraged tickers by boundary setting.
- As the boundary spread increases from – 2.0 percent through 4.0 percent to -10.0 percent through 20.0 percent, the average percentage return increases from 107.41 percent to 183.02 percent.
- However, there is a reversal in the average percentage return for the lower and upper boundary values with the widest spread (Lower: -12.50% | Upper: +25.00%). For this pair of boundary values, the average percentage return decreases to 153.92 from the next widest boundary spread with an average percentage return.
- In other words, the Lower: -10.00% | Upper: +20.00% boundary category returned the best performance for this set of five unleveraged tickers.
- The values in the avg leveraged portfolio return percent column reveal the average return percent across all five leveraged tickers by boundary settings.
- The overall results for leveraged ETFs reflected the same pattern across boundary pairs with
- the Lower: -10.00% | Upper: +20.00% boundary category returning the best performance outcome,
- the Lower: -2.00% | Upper: +4.00% boundary category returning the worst performance outcome, and
- the Lower: -12.50% | Upper: +25.00% boundary category exhibiting a reversal in average return percent relative to the Lower: -10.00% | Upper: +20.00% boundary category.
- From a trader’s perspective, the most significant outcome from the following table is that the leveraged ETFs significantly outperformed their unleveraged counterparts by over 200 up to 545.60 percentage points across lower/upper boundary spreads.
- Additionally, the best outcomes were achieved for the Lower: -10.00% | Upper: +20.00% boundary category for both unleveraged and leveraged boundary categories.
- The top two rows have a green background to signify that they expose boundary categories with average portfolio returns results that are beyond those of the remaining table rows.
The second table highlights average portfolio return percentage on a per trade basis.
- This table shows that the Lower: -12.50% | Upper: +25.00% boundary category yields the best performance relative to all other lower and upper boundary categories.
- This finding is caused by there being fewer trades for the Lower: -12.50% | Upper: +25.00% boundary category relative to all other categories.
- Even though the Lower: -10.00% | Upper: +20.00% boundary category has the largest overall percentage, it costs 15 additional trades to obtain that result. If you are tracking a large number of tickers and/or many clients, this may be enough of a factor to prefer the Lower: -12.50% | Upper: +25.00% boundary category over the Lower: -10.00% | Upper: +20.00% category.
- The top two rows have a green background to signify that they expose boundary categories with larger return % per trade than any of the remaining table rows.
Return Results by Boundary Settings Within Asset Pairs
With the data collected for this post, it is possible to quantify the impact of lower and upper price boundaries on return percentages at a more granular level than just the portfolio level. We can do this by examining return percentages for each pair of lower and upper boundary values within each of the five asset pairs of unleveraged and leveraged ETFs.
The following table lists the percentage return values for the cross of each asset boundary with each asset pair. There are twenty-five rows in the table’s body because the table is based on five asset_pair values crossed with five boundary settings.
- The identifiers for each row appear in the asset_pair and boundary_settings columns.
- The remaining four sets of column values help to identify the type of impact that the row identifiers have on performance.
- The total_trades column values reveal how efficient a lower and upper boundary pair is at finding profitable trades. A lower number of trades means the model is more efficient at finding profitable trades because it requires fewer trades to achieve its returns.
- The total summed unleveraged return % and total summed leveraged return % column values show the magnitude of the return percentages for unleveraged and leveraged ETFs, respectively, on each row. The larger the values, the greater the impact of boundary settings for an asset pair.
- The leveraged return advantage column values reflect the total summed leveraged return % column value less the total summed unleveraged return % column value for each row.
The following table extracts from the preceding table the best
total summed leveraged percentage return for the cross of each boundary setting
within each asset_pair.
The first four data rows of the table have a background
color of green to signify that these four best return rows have a
boundary_setting column value of either Lower: -12.50% | Upper: +25.00% or Lower:
-10.00% | Upper: +20.00%. This outcome is
broadly consistent with the outcome from the preceding section that shows the
best portfolio return percentages were returned with boundary_setting column
values of either Lower: -10.00% | Upper: +20.00% or Lower: -12.50% | Upper:
+25.00%. Unsurprisingly, the asset_pair level
return percentages generally correspond to portfolio return percentages.
However, it is worth noting that the iwm/tna asset_pair did
not have its boundary_setting for a best return percentage match the best
boundary_setting for the other four asset_pair values. This outcome indicates that you should not
assume that you will obtain similar best boundary_setting values for any other set
of ticker values that you may want to trade.
Hopefully, the Code Appendix in this post and related other
posts from this blog will equip you with the skills to assess return percentages
for other assets besides those used in this post. In any event, it is possible that based on
another prior post (A
Preliminary Analysis of EMA Period Lengths and Price Action in a Buy-Sell Model)
a boundary_setting of Lower: -5.00% | Upper: +10.00% will work acceptably well,
if not optimally, for other assets.
Concluding Comments
The primary goal of this post is to verify the ability of the POGL model to specify winning trades based on it’s lower and upper boundary settings. This post reports portfolio return percentages based on portfolios of tickers for leveraged versus unleveraged ETFs as well as asset_pair based return percentages for five boundary settings within each asset pair. Concurrently, the post examines the impact of different boundary_setting specifications across both unleveraged and leveraged ETFs based on major market indexes.
Overall, the top two best performing boundary settings were Lower: -10.00% | Upper: +20.00% and Lower: -12.50% | Upper: +25.00%. This finding was very robust at both the portfolio and individual asset_pair-level. Please keep in mind that one of the five asset_pairs tracked in this post did not follow the general pattern. Additional research is needed with either other sets of asset pairs or individual sets of assets to determine the superiority of the Lower: -10.00% | Upper: +20.00% and Lower: -12.50% | Upper: +25.00% boundary settings.
Another systematic outcome is that leveraged ETFs generally had substantially superior return percentages relative to their unleveraged counterparts. This is almost surprising in the sense that the POGL model was never directly applied to leveraged ETFs. Instead, the POGL trading model was applied to the unleveraged ETFs to derive entry and exit dates for successive trades. Then, those same dates were applied to the underlying historical prices and EMAs for leveraged ETFs. When working with matched pairs of security tickers, this is the standard approach to use.
The coders who follow this blog may be especially interested in the Code Appendix below, which includes a T-SQL script for performing the backtest analysis. The introduction to the appendix includes some remarks to help motivated readers to understand and modify the code to track other asset pairs besides those tracked in this post.
Code Appendix: T-SQL Parametric Backtest Engine & Reporting
- POGL Model Simulation & Parameter Sweeps (dbo.sp_run_pogl_backtest): The script creates a clean results table (dbo.pogl_parametric_results) and deploys a cursor-driven stored procedure that iterates through historical price and exponential moving average data (dbo.stockhistory_with_emas). For each targeted asset pair, the procedure detects entry signals when the daily close price is above the 21 EMA while the 21 EMA is aligned above the 100 EMA, triggering position entry on the next day's open. Once in position, an internal loop evaluates daily close prices against dynamic lower (stop-loss) and upper (profit ratchet) boundary thresholds. Completed trade lifecycles—recording entry/exit dates, trade counts, and percentage gains for both standard and leveraged tickers—are logged directly to the results table across five progressive boundary sweeps (ranging from -2.0%/+4.0% to -12.5%/+25.0%).
- Ticker Pair Summary Query Result Set (Asset-Level Hierarchy): The first reporting query aggregates completed trade logs at the asset-pair level (group by parameter_set, u_ticker, e_ticker). It computes total trades executed, cumulative percentage returns for standard and leveraged ETFs, and the net leveraged return advantage. The result set is ordered by asset pair and descending by leveraged return advantage, grouping all five parameter settings beneath each respective ticker pair to reveal parameter sensitivity per asset class.
- Macro Portfolio Summary Query Result Set (Boundary-Level Rollup): The second reporting query uses a two-stage aggregation pipeline to evaluate top-level portfolio performance across boundary settings. An inner subquery summarizes trade totals and returns per asset pair, while the outer query calculates portfolio-wide averages (avg unleveraged portfolio return % and avg leveraged portfolio return %) and normalized per-trade metrics (avg portfolio return % per trade across the 5-pair capital allocation). The final output orders boundary settings descending by overall leveraged portfolio return advantage to rank parameter efficiency from highest to lowest.
use securitytradinganalytics;
go
-- step 0: recreate table with exact column structure to
avoid schema conflicts
if object_id('dbo.pogl_parametric_results') is not null
drop table
dbo.pogl_parametric_results;
create table dbo.pogl_parametric_results (
parameter_set
varchar(50),
u_ticker
varchar(10),
e_ticker
varchar(10),
b_date date,
e_date date,
u_return_pct
decimal(18,4),
e_return_pct
decimal(18,4),
levels int
);
go
-- step 1: create or alter stored procedure for boundary
sweeps
create or alter procedure dbo.sp_run_pogl_backtest
@lower_boundary_pct decimal(5,4) = -0.0200, -- e.g., -0.0200 for -2%
stop-loss
@upper_boundary_pct decimal(5,4) = 0.0400 -- e.g., 0.0400 for +4% upper ratchet
as
begin
set nocount on;
-- parameter
label formatting (e.g., 'stop: -2.00% | ratchet: +4.00%')
declare
@param_label varchar(50) =
'stop: ' +
cast(cast(@lower_boundary_pct * 100 as decimal(5,2)) as varchar) + '% | ' +
'ratchet:
+' + cast(cast(@upper_boundary_pct * 100 as decimal(5,2)) as varchar) + '%';
-- map target
asset pairs
if
object_id('tempdb..#ticker_map') is not null drop table #ticker_map;
create table
#ticker_map (u_ticker varchar(10), e_ticker varchar(10));
insert into
#ticker_map values
('dia_gs',
'udow'),
('spy', 'spxl'),
('qqq', 'tqqq'),
('soxx', 'soxl'),
('iwm', 'tna');
declare @u_tkr
varchar(10), @e_tkr varchar(10);
declare
cur_pairs cursor local fast_forward for select u_ticker, e_ticker from
#ticker_map;
open cur_pairs;
fetch next from
cur_pairs into @u_tkr, @e_tkr;
while
@@fetch_status = 0
begin
if
object_id('tempdb..#staged_loop_data') is not null drop table
#staged_loop_data;
select
row_number() over (order by [date]) as row_id,
[date],
[open],
[close],
ema_21
as ema_fast,
ema_100
as ema_slow,
lead([open]) over (order by [date]) as nxt_o
into
#staged_loop_data
from
dbo.stockhistory_with_emas
where
ticker = @u_tkr
and
ema_100 is not null
order by
[date];
declare @i
int = 1, @max_i int = (select count(*) from #staged_loop_data);
declare
@in_pos bit = 0;
declare
@u_ent decimal(18,4), @b_dt date;
declare
@low_b decimal(18,4), @up_b decimal(18,4), @lvls int;
while @i
<= @max_i
begin
declare
@d date, @uc decimal(18,4), @ue_fast decimal(18,4), @ue_slow decimal(18,4),
@unxto decimal(18,4);
select
@d = [date], @uc = [close], @ue_fast = ema_fast, @ue_slow = ema_slow, @unxto =
nxt_o
from
#staged_loop_data
where
row_id = @i;
------------------------------------------------------------------
--
STATE A: LOOKING FOR NEW TRADE ENTRY SIGNAL (OUT OF MARKET)
------------------------------------------------------------------
if
@in_pos = 0
begin
if
@uc > @ue_fast and @ue_fast > @ue_slow and @unxto is not null
begin
set @in_pos = 1;
set @b_dt = @d;
set @u_ent = @unxto;
set @low_b = @u_ent * (1.0 + @lower_boundary_pct);
set @up_b = @u_ent * (1.0 +
@upper_boundary_pct);
set @lvls = 0;
end
end
------------------------------------------------------------------
--
STATE B: MANAGING ACTIVE POSITION (IN MARKET)
------------------------------------------------------------------
else
begin
if
@uc >= @up_b
begin
set @lvls = @lvls + 1;
set @low_b = @up_b * (1.0 + @lower_boundary_pct);
set @up_b = @up_b * (1.0 +
@upper_boundary_pct);
end
if
@uc <= @low_b and @unxto is not null
begin
declare @e_ent decimal(18,4), @e_exit decimal(18,4);
set @e_ent = (
select [open]
from dbo.stockhistory_with_emas
where ticker = @e_tkr
and [date] = (
select [date]
from
#staged_loop_data
where row_id =
(select row_id + 1 from #staged_loop_data where [date] = @b_dt)
)
);
set @e_exit = (
select [open]
from dbo.stockhistory_with_emas
where ticker = @e_tkr
and [date] = (
select [date]
from
#staged_loop_data
where row_id =
(select row_id + 1 from #staged_loop_data where [date] = @d)
)
);
if @e_ent is not null and @e_exit is not null
begin
insert into dbo.pogl_parametric_results (
parameter_set,
u_ticker, e_ticker, b_date, e_date,
u_return_pct,
e_return_pct, levels
)
values (
@param_label,
@u_tkr,
@e_tkr,
@b_dt,
@d,
((cast(@unxto as
float) - @u_ent) / @u_ent) * 100.0,
((cast(@e_exit as
float) - @e_ent) / @e_ent) * 100.0,
@lvls
);
end;
set @in_pos = 0;
end
end
set @i
= @i + 1;
end;
fetch next
from cur_pairs into @u_tkr, @e_tkr;
end;
close
cur_pairs;
deallocate
cur_pairs;
end;
go
-- step 2: clear out results repository
truncate table dbo.pogl_parametric_results;
-- step 3: run 5 progressive boundary sweeps
exec dbo.sp_run_pogl_backtest @lower_boundary_pct =
-0.0200, @upper_boundary_pct = 0.0400;
exec dbo.sp_run_pogl_backtest @lower_boundary_pct =
-0.0500, @upper_boundary_pct = 0.1000;
exec dbo.sp_run_pogl_backtest @lower_boundary_pct =
-0.0750, @upper_boundary_pct = 0.1500;
exec dbo.sp_run_pogl_backtest @lower_boundary_pct =
-0.1000, @upper_boundary_pct = 0.2000;
exec dbo.sp_run_pogl_backtest @lower_boundary_pct =
-0.1250, @upper_boundary_pct = 0.2500;
-------------------------------------------------------------------------
-- view 1: detailed results per asset pair sorted by
leveraged return %
-------------------------------------------------------------------------
select
u_ticker + '/'
+ e_ticker as [asset_pair],
parameter_set
as [boundary_setting],
count(*) as
[total_trades],
cast(sum(u_return_pct) as decimal(18,2)) as [total summed unleveraged
return %],
cast(sum(e_return_pct) as decimal(18,2)) as [total summed leveraged
return %],
cast(sum(e_return_pct) - sum(u_return_pct) as decimal(18,2)) as
[leveraged return advantage]
from dbo.pogl_parametric_results
group by parameter_set, u_ticker, e_ticker
order by [asset_pair], [leveraged return advantage] desc;
-------------------------------------------------------------------------
-- view 2: macro aggregate portfolio summary with
per-trade averages
-------------------------------------------------------------------------
select
parameter_set
as [boundary_setting],
sum(total_trades) as [grand total trades],
-- Portfolio
Averages (Average cumulative return per ticker pair across the 5 pairs)
cast(avg(total_u_return) as decimal(18,2)) as [avg unleveraged portfolio
return %],
cast(avg(total_e_return) as decimal(18,2)) as [avg leveraged portfolio
return %],
cast(avg(total_e_return) - avg(total_u_return) as decimal(18,2)) as
[leveraged portfolio return advantage],
-- Portfolio
Per-Trade Averages (Portfolio return scaled across 5 ticker pairs divided by
total trades executed)
cast(sum(total_u_return) / sum(total_trades) as decimal(18,2)) as [avg
unleveraged portfolio return % per trade],
cast(sum(total_e_return) / sum(total_trades) as decimal(18,2)) as [avg
leveraged portfolio return % per trade]
from (
select
parameter_set,
u_ticker,
e_ticker,
count(*) as
total_trades,
sum(u_return_pct) as total_u_return,
sum(e_return_pct) as total_e_return
from
dbo.pogl_parametric_results
group by
parameter_set, u_ticker, e_ticker
) as stage
group by parameter_set
order by [leveraged portfolio return advantage] desc;
Comments
Post a Comment