Validating Market Regimes Across Six Tickers
This is the second post in a multi-part series on tracking six tickers across multiple decades with different buy-sell models. The first post in the series illustrates how to collect historical prices with the GOOGLEFINANCE function in Google Sheets for six tickers and transfer the historical prices to a SQL Server database in order to achieve downstream modeling and analysis objectives. The six tickers for the post series are CAT, JPM, LLY, MSFT, NVDA, and SHW. The first post in the series includes a brief section describing the company for each ticker.
This second post segments that same price history into a sequence of eras and assigns each one a regime: expansion, contraction, or shock. Driven by favorable market, economic, or societal conditions, an expansion era consistently generates higher prices at its close than at its beginning. Contraction or shock eras should end with lower prices than when they start because of events that happen during either one of these era types. What separates contraction eras from shock eras is duration — a shock era denotes a short, sharp price drop, and a contraction era represents a price drop over a longer duration.
Validation refers to the process of reconciling historical price changes for eras to the labels assigned to eras. The validation process can run across all six tickers or individual tickers. This post demonstrates the process for validating the process across all six tickers. However, the appendix titled "A.4: Section 2 — Validation By Individual Ticker" towards the end of this post contains a sql script for performing for an individual ticker.
Later in this post series, the validated regime labels for eras will provide a framework for assessing buy-sell model performance relative to a buy-and-hold strategy.
The Eras for this Post
A total of 21 eras are defined for the dataset in this post. The following table lists each era in chronological order (sequence 1 through 21), along with descriptive titles and notes characterizing the primary market drivers. For example, the first era is associated with the 1973 OPEC oil embargo, the fourteenth era was driven by the COVID-19 pandemic, and the twentieth era reflects market dislocations during the intensive 2026 U.S./Israel bombing of Iran.
Each era is assigned a market regime label. Generally, an expansion era is followed by a contraction or shock era, though consecutive expansions can occur (e.g., Eras 6 and 7). The transition between eras is dictated by the specific market, macroeconomic, or societal events that define their start and end dates.
Era 21 remains open because price data collection ends on 2026-07-31 without a terminating structural event. Because an open era lacks a settled conclusion, including it would distort duration-normalized metrics. Therefore, while 21 eras are defined in the ledger, only the 20 closed eras (Eras 1–20) are evaluated in the validation analysis.
| Era, Title, Regime, and Descriptive Note | |||
|---|---|---|---|
| Era | Title | Regime | Descriptive Note |
| 1 | Oil-Shock Bear Market | Contraction | 1973 OPEC oil embargo and stagflation drove a prolonged bear market. |
| 2 | Stagflation Recovery | Expansion | Extended recovery through a decade marked by persistent inflation. |
| 3 | Volcker Recession | Contraction | Fed rate hikes to break inflation triggered a sharp recession. |
| 4 | 1980s Expansion | Expansion | Sustained bull market following Volcker-era disinflation. |
| 5 | Black Monday & Aftermath | Shock | Oct 19, 1987 single-day crash (−22.6%) and its immediate aftermath. |
| 6 | Late-80s–Pre-Dot-Com Run | Expansion | Extended recovery and bull run following the 1987 crash. |
| 7 | Dot-Com Build-Up | Expansion | Tech-bubble-inflating bull run. |
| 8 | Dot-Com Bust | Contraction | Tech bubble collapse and prolonged bear market. |
| 9 | Credit-Fueled Bull Run | Expansion | Housing- and credit-driven expansion preceding the GFC. |
| 10 | Global Financial Crisis | Contraction | Subprime mortgage collapse and global financial crisis. |
| 11 | Post-GFC Bull Market | Expansion | Extended post-crisis recovery and bull market. |
| 12 | 2018 Trade-War Correction | Shock | Fed tightening fears plus U.S.–China trade-war rhetoric drove a sharp Q4 selloff. |
| 13 | Pre-COVID Recovery | Expansion | Recovery from the Dec 2018 correction through the pre-pandemic market peak. |
| 14 | COVID Crash | Shock | Pandemic-driven crash — the fastest bear market on record at the time. |
| 15 | COVID Recovery / Bull Run | Expansion | Stimulus-fueled recovery and bull run. |
| 16 | 2022 Rate-Hike Bear Market | Contraction | Fed rate-hiking cycle to combat inflation. |
| 17 | AI-Driven Bull Run | Expansion | AI-capex-driven bull market. |
| 18 | 2025 Tariff Shock | Shock | "Liberation Day" tariff announcement (Apr 2) through Trump's 90-day pause and the resulting +9.5% rally (Apr 9) — the S&P's biggest one-day gain since 2008. |
| 19 | 2025–26 Recovery Rally | Expansion | Tariff-shock recovery to new highs. Absorbs the June 2025 Twelve-Day War (Israel/US strikes on Iran) — a real one-day shock that fully round-tripped within two weeks, leaving no lasting break. |
| 20 | 2026 Intensive Iran Bombing | Shock | U.S./Israeli strikes on Iran (Operation Epic Fury / Roaring Lion, began Feb 28, 2026) through the day before the initial ceasefire. S&P still down 3.3% YTD on this era's last day. |
| 21 | 2026 Post-Bombing Recovery | Expansion | Absorbs the naval blockade, failed Islamabad talks, and the June 17 MOU signing — none left a lasting price break. Unlike other eras in the dataset for this post, this era is not closed. Its end date merely marks the end of data collection (2026-07-31) not the end of an era. |
The next table presents two additional bits of information about each era besides that presented in the preceding table. For example, the Date range column values denote the start and end dates for each era in YYYY-MM-DD format. The Duration column values denote the duration of an era in years, months, or days. Notice that era 21 Duration column value is 3.8 mo+. This is another reminder that era 21 is not yet closed.
| Era, Title, Regime, Date Range, and Duration | ||||
|---|---|---|---|---|
| Era | Title | Regime | Date range | Duration |
| 1 | Oil-Shock Bear Market | Contraction | 1972-06-01 → 1974-12-06 | 2.5 yr |
| 2 | Stagflation Recovery | Expansion | 1974-12-07 → 1981-06-30 | 6.6 yr |
| 3 | Volcker Recession | Contraction | 1981-07-01 → 1982-08-11 | 1.1 yr |
| 4 | 1980s Expansion | Expansion | 1982-08-12 → 1987-10-18 | 5.2 yr |
| 5 | Black Monday & Aftermath | Shock | 1987-10-19 → 1987-12-31 | 2.4 mo |
| 6 | Late-80s–Pre-Dot-Com Run | Expansion | 1988-01-01 → 1995-12-31 | 8.0 yr |
| 7 | Dot-Com Build-Up | Expansion | 1996-01-01 → 2000-03-10 | 4.2 yr |
| 8 | Dot-Com Bust | Contraction | 2000-03-11 → 2002-10-09 | 2.6 yr |
| 9 | Credit-Fueled Bull Run | Expansion | 2002-10-10 → 2007-10-09 | 5.0 yr |
| 10 | Global Financial Crisis | Contraction | 2007-10-10 → 2009-03-09 | 1.4 yr |
| 11 | Post-GFC Bull Market | Expansion | 2009-03-10 → 2018-09-20 | 9.5 yr |
| 12 | 2018 Trade-War Correction | Shock | 2018-09-21 → 2018-12-24 | 3.1 mo |
| 13 | Pre-COVID Recovery | Expansion | 2018-12-25 → 2020-02-19 | 1.2 yr |
| 14 | COVID Crash | Shock | 2020-02-20 → 2020-03-23 | 33 days |
| 15 | COVID Recovery / Bull Run | Expansion | 2020-03-24 → 2022-01-03 | 1.8 yr |
| 16 | 2022 Rate-Hike Bear Market | Contraction | 2022-01-04 → 2022-10-12 | 9.3 mo |
| 17 | AI-Driven Bull Run | Expansion | 2022-10-13 → 2025-04-01 | 2.5 yr |
| 18 | 2025 Tariff Shock | Shock | 2025-04-02 → 2025-04-09 | 8 days |
| 19 | 2025–26 Recovery Rally | Expansion | 2025-04-10 → 2026-02-27 | 10.6 mo |
| 20 | 2026 Intensive Iran Bombing | Shock | 2026-03-02 → 2026-04-07 | 37 days |
| 21 | 2026 Post-Bombing Recovery | Expansion | 2026-04-08 → 2026-07-31 | 3.8 mo+ |
Computing the Era/Regime Metrics
The eras and regimes just presented are only labels until they're checked against what actually happened to each ticker's price. The dbo.regime_ticker_metrics table makes that assessment possible. This table calculates three metrics (overall_pct_return, annualized_pct_return, and max_drawdown_pct) across the 111 unique era-ticker combinations available in the 20 closed eras.
- Overall_pct_return is the percentage price change from the era’s starting close price to its ending close price..
- Annualized_pct_return is the Compound Annual Growth Rate (CAGR) normalized using a standard 252-trading-day year.
- Max_drawdown_pct is the maximum peak-to-trough percentage decline recorded from any daily running peak within that era.
The values in the dbo.regime_ticker_metrics table depend on a joined data source of the dbo.stockhistory_clean_gs table with both the dbo.all_tickers_shared_dates and the dbo.market_regime_ledger tables. The following script has four main sections.
- Section 1 contains the table joins for the joined data source and computes the running peak and the first/last-day markers for the current table row.
- Section 2 computes each day's drawdown from the running peak for the current table row.
- Section 3 aggregates the daily rows into one row per era and ticker, capturing the trading-day count, the era's start and end dates, the start and end close prices, and the worst drawdown recorded during the era.
- Section 4 calculates the final percentage metrics from the aggregated boundary prices, excludes invalid or missing start prices, filters out the open Era 21, and materializes the final output into dbo.regime_ticker_metrics.
use SecurityTradingAnalytics
go
-- Conditionally drop the prior materialized result set
before rebuilding it.
-- This needs its own batch (GO) -- a WITH clause must be
the first statement
-- in its batch, so this can't just be a
semicolon-terminated statement ahead
-- of the CTE chain below.
if object_id('dbo.regime_ticker_metrics', 'U') is not
null
drop table
dbo.regime_ticker_metrics;
go
-- Regime x ticker summary metrics
-- Revision (2026-08-31): replaced
max_single_day_loss_pct with max_drawdown_pct
-- (worst peak-to-trough decline within the era, not just
the single worst day) --
-- drawdown is the standard tail-risk metric, and it's
directly comparable to
-- POGL's own pullback-from-peak exit logic in a way a
single-day return isn't.
-- 1. join clean price history on shared dates directly
to the market regime ledger
with era_prices as (
select
m.era_id,
m.era_name,
m.regime,
gs.ticker,
gs.[date],
gs.[close],
max(gs.[close]) over (
partition by m.era_id, gs.ticker
order
by gs.[date]
rows
between unbounded preceding and current row
) as
running_peak_close,
row_number() over (
partition by m.era_id, gs.ticker
order
by gs.[date] asc
) as
seq_asc,
row_number() over (
partition by m.era_id, gs.ticker
order
by gs.[date] desc
) as
seq_desc
--
dbo.stockhistory_clean_gs is the historical price
--
table for the tickers in this post; see the post at
--
https://securitytradinganalytics.blogspot.com/2026/08/a-multi-decade-framework-for-comparing.html
--
for the script to create and populate the table
from
dbo.stockhistory_clean_gs gs
--
see a script for creating and populating
--
dbo.all_tickers_shared_dates in a post appendix
inner join
dbo.all_tickers_shared_dates d
on
gs.[date] = d.[date]
inner join
dbo.market_regime_ledger m
on
gs.[date] >= m.start_date
and
gs.[date] <= m.end_date
where gs.ticker
in ('CAT', 'JPM', 'LLY', 'SHW', 'MSFT', 'NVDA')
),
-- 2. each day's drawdown from the running peak within
its own era
daily_drawdowns as (
select
era_id,
era_name,
regime,
ticker,
[date],
[close],
seq_asc,
seq_desc,
case
when
running_peak_close > 0
then
([close] - running_peak_close) / running_peak_close
else
null
end as
drawdown_pct
from era_prices
),
-- 3. aggregate era boundary prices, total days, and
worst drawdown
era_boundaries as (
select
era_id,
era_name,
regime,
ticker,
count(*) as
trading_days,
min([date])
as era_start_date,
max([date])
as era_end_date,
max(case
when seq_asc = 1 then [close] end) as start_close,
max(case
when seq_desc = 1 then [close] end) as end_close,
min(drawdown_pct) as max_drawdown
from
daily_drawdowns
group by
era_id,
era_name,
regime,
ticker
)
-- 4. final calculations, materialized into
dbo.regime_ticker_metrics so the
-- section 1 and
section 2 appendix scripts have a stable table to query
-- against
instead of re-running this whole CTE chain every time.
select
era_id,
era_name,
regime,
ticker,
trading_days,
era_start_date,
era_end_date,
start_close,
end_close,
-- overall
percentage return across the era
cast(((end_close - start_close) / start_close) * 100.0 as numeric(10,
2)) as [overall_pct_return],
-- annualized
percentage return (cagr) using 252 trading days/year
cast((power(cast(end_close / start_close as float), (252.0 / nullif(trading_days,
0))) - 1.0) * 100.0 as numeric(10, 2)) as [annualized_pct_return],
-- worst
peak-to-trough decline within the era (replaces max_single_day_loss_pct)
cast(max_drawdown * 100.0 as numeric(10, 2)) as [max_drawdown_pct]
into dbo.regime_ticker_metrics
from era_boundaries
where start_close is not null
and start_close
> 0
and era_id
<> 21
order by
ticker,
era_id;
go
/*
-- optionally echo return set
select *
from dbo.regime_ticker_metrics
order by
ticker,
era_id;
*/
The following screenshot displays separate rows for the twenty unique era-regime-ticker combinations for the CAT ticker in the dbo.regime_ticker_metrics table.
- The first four columns show the era_id, era_name, regime, and ticker values for each unique combination.
- The fifth column reveals the count of trading days for the era-regime-ticker combination of the current row.
- The sixth and seventh columns show the first and last dates within the current row's era for which CAT has recorded price data — not necessarily the era's exact calendar boundary dates from the earlier table, since the two can differ by a few days when a boundary falls on a weekend, holiday, or other non-trading gap.
- The eighth and ninth columns show the close prices on those two trading dates.
- The last three columns reveal the metric values for assessing how well each row confoms to its regime expectation.
- Overall_pct_return is the percentage change for ending close value relative to the beginning close value for each era-regime pair.
- Annualized_pct_return calculates the constant annual rate of growth between the beginning and ending date within each era-regime pair.
- Max_drawdown_pct is the worst-case peak-to-trough decline for daily close values in an era-regime pair.
There are also twenty-row sets in the dbo.regime_ticker_metrics table for the JPM, LLY, and SHW tickers. The dbo.regime_ticker_metrics table also contains 17 and 14 row sets, respectively, for the MSFT and NVDA tickers. The following screenshot displays the row set for the NVDA ticker.
- Notice there are just 14 rows in the NVDA row set.
- There are two reasons for this:
- NVDA data does not start until era 7, and
- era 21 is not closed by a terminating event (just the cessation of historical price collection)
As the title of this post implies, the main objective of the current post is to validate the regime assignments for eras across the six-ticker set tracked in this post. This section presents two tabular result sets that highlight key findings across the six tickers.
Recall that the process for downloading historical open and close prices for each of the six tickers is described in an earlier post titled “A Multi-Decade Framework for Comparing Ticker Prices from Two Spreadsheet Data Providers”. That earlier post accomplished two main objectives:
- First, it downloaded historical prices for the six tickers in this post with the GOOGLEFINANCE function in Google Sheets and the STOCKHISTORY function in Microsoft Excel. It turned out that the returns from GOOGLEFINANCE function were more appropriate for the requirements of this post. Therefore, this post uses historical close prices from just the GOOGLEFINANCE function.
- Second, the earlier post inserted the downloaded historical prices from GOOGLEFINANCE into a SQL Server table named dbo.stockhistory_clean_gs. This table is referenced by several code samples in this post, including Appendix A.3: Section 1 — Validation Across All Tickers. The code in the appendix generates results for this section.
The preceding section in this post performs two primary functions. First, it calculates overall_pct_return, annualized_pct_return, and max_drawdown_pct metric values for each of the 111 era-ticker combinations available for analysis. Furthermore, each of the 111 era-ticker combinations is categorized under one of three regimes: Expansion, Contraction, or Shock. The Appendix A.3: Section 1 — Validation Across All Tickers includes code for calculating the median value for each metric for each regime. The following table shows the outcome of these calculations.
- Era-ticker pairs with an expansion regime had a median overall_pct_return (121.955), which was large and substantially more positive versus era-ticker pairs having contraction or shock regimes, respectively (-39.12 and -8.53). These results clearly validate that overall_pct_return performance grows during expansion regimes and contracts during contraction and shock regimes.
- The median max_drawdown_pct is negative for era-ticker pairs across all three regimes.
- Era-ticker pairs with a contraction regime have the most negative median max_drawdown_pct value.
- At first glance, it may seem counterintuitive that Expansion regimes exhibit a deeper median maximum drawdown (-27.48%) than Shock regimes (-18.58%). However, this is primarily driven by duration and opportunity: Expansion eras span years (allowing substantial intra-era pullbacks during a broader bull run), whereas Shock eras last only days or weeks, limiting the time window for peak-to-trough losses to accumulate. Conversely, Contraction regimes—which combine multi-year duration with sustained bear markets—exhibit the severe drawdown (-47.43%).
- The medians for daily_pct_rate and annualized_pct_return values exhibit corresponding patterns across regimes.
- Era-ticker pairs with a shock regime have the most negative value for both medians.
- Era-ticker pairs with an expansion regime both have positive median values.
- Finally, era-ticker pairs with a contraction regime have medians that are between the median values for the other two regimes.
- These patterns also clearly validate the assignment of regimes to era-ticker combinations.
A second check on the regime assignments looks at whether each era-ticker pair's return sign matches what its regime implies: a positive overall_pct_return for an expansion pair, a negative one for a contraction or shock pair. Anything else counts as a mismatch. The following result set reports, for each regime, the number of era-ticker pairs, the mismatch count, and the mismatch percentage.
Mismatch rates rise from expansion (5.4%) to contraction
(15.4%) to shock (20.7%), point in the same direction as the median metrics
above. This check is a cruder instrument than that analysis, though — it only
looks at whether a return crossed zero, not by how much, so a return that
barely missed its expected sign counts the same as one that missed it badly.
Treat it as a secondary data point that's broadly consistent with the median
results, not as independent confirmation of them.
Next Steps
This post is the second one in a multi-part series on collecting, analyzing, and modeling muti-decade historical data series. While this post focuses on validating names and dates for historical eras, the first post in the series presents a framework for collecting multi-decade historical price series.
This post validated the eras for historical data going back to the early 1970’s through slightly past the middle of 2026. Each era was assigned a regime label of expansion, contraction, or shock. A historical price analysis supported the validity of the era dates and regimes in the sense that market performance for tickers tracked in this post generally matched the performance of the six-ticker set tracked in this post.
Subsequent posts in the series will highlight different modeling approaches for the historical price series and eras discussed in this post. The models are for automating the designation of when to enter and exit a trade. The next post will illustrate the use of the Proper Order and Gain Lock In (POGL) model for the tickers and eras discussed in this post. For those who would like to learn more about the POGL, you can ramp up to speed by scanning any of these three prior posts.
- A Preliminary Analysis of EMA Period Lengths and Price Action in a Buy-Sell Model
- Posts On Single-Stock ETFs and Modeling Buy-Sell Decisions
- Gain Lock In Backtests for Trading ETFs Based on Major Market Indexes
Up to two additional models will be described and fitted to the historical data collected in the first post within the series. Finally, the last post in the series will summarize accomplishments of all prior series posts and include a deep audio dive into the series’ content for those who prefer to learn my listening.
Appendix A.1: Market Regime Ledger — Table and Population Script
The dbo.market_regime_ledger table backs every era/regime
figure shown earlier in this post; this script creates it and loads the 21
eras.
use SecurityTradingAnalytics
go
-- Conditionally drop any prior version before rebuilding
it.
if object_id('dbo.market_regime_ledger', 'U') is not null
drop table
dbo.market_regime_ledger;
go
create table dbo.market_regime_ledger (
era_id int not null primary key,
era_name varchar(60)
not null,
regime varchar(20) not null check (regime in ('Expansion',
'Contraction', 'Shock')),
start_date
date not null,
end_date date
not null,
check (end_date
>= start_date)
);
go
-- Populates dbo.market_regime_ledger with the 21 eras
from the Market Regime
-- Ledger. Rows 1-17 as corrected 2026-08-22 (Era 3/4
boundary moved to the
-- Dow's actual closing low, Aug 12 1982, replacing the
NBER recession end
-- date; Era 11 and Era 15 boundaries source-confirmed).
Rows 18-21 replace
-- the old, too-coarse single Era 18 ("2025-26
Tariff Era (open)") as of the
-- 2026-08-25 revision, splitting it into the Apr 2026
tariff shock, the
-- recovery rally, the 2026 Iran bombing shock, and the
still-open
-- post-bombing recovery.
--
-- KNOWN OPEN ITEM: Era 19 end (2026-02-27) to Era 20
start (2026-03-02) is a
-- 3-calendar-day gap, not the 1-day contiguity every
other boundary in this
-- table follows. A later source check (Wikipedia,
"Timeline of the 2026 Iran
-- war") puts the war's actual start at 2026-02-28,
one day earlier than the
-- 2026-03-02 boundary this table uses -- so the gap is a
real, not-yet-
-- resolved discrepancy, not a typo. Also still open:
whether the "2026
-- Post-Bombing Recovery" era (21) needs splitting
given the ceasefire was
-- reported as repeatedly violated (renewed strikes
~2026-05-07 and
-- ~2026-07-29) within what this table treats as one
uninterrupted Expansion.
--
-- Otherwise, no gaps or overlaps: era_id N's end_date is
always the day
-- before era_id N+1's start_date.
insert into dbo.market_regime_ledger (era_id, era_name,
regime, start_date, end_date)
values
(1, 'Oil-Shock Bear Market', 'Contraction', '1972-06-01',
'1974-12-06'),
(2, 'Stagflation Recovery', 'Expansion', '1974-12-07', '1981-06-30'),
(3, 'Volcker Recession', 'Contraction', '1981-07-01',
'1982-08-11'),
(4, '1980s Expansion', 'Expansion', '1982-08-12', '1987-10-18'),
(5, 'Black Monday & Aftermath', 'Shock', '1987-10-19', '1987-12-31'),
(6, 'Late-80s-Pre-Dot-Com Run', 'Expansion', '1988-01-01', '1995-12-31'),
(7, 'Dot-Com Build-Up', 'Expansion', '1996-01-01', '2000-03-10'),
(8, 'Dot-Com Bust', 'Contraction',
'2000-03-11', '2002-10-09'),
(9, 'Credit-Fueled Bull Run', 'Expansion', '2002-10-10', '2007-10-09'),
(10, 'Global
Financial Crisis',
'Contraction', '2007-10-10', '2009-03-09'),
(11, 'Post-GFC
Bull Market',
'Expansion', '2009-03-10',
'2018-09-20'),
(12, '2018
Trade-War Correction',
'Shock', '2018-09-21',
'2018-12-24'),
(13, 'Pre-COVID
Recovery',
'Expansion', '2018-12-25',
'2020-02-19'),
(14, 'COVID
Crash',
'Shock', '2020-02-20',
'2020-03-23'),
(15, 'COVID
Recovery / Bull Run',
'Expansion', '2020-03-24',
'2022-01-03'),
(16, '2022
Rate-Hike Bear Market',
'Contraction', '2022-01-04', '2022-10-12'),
(17, 'AI-Driven
Bull Run',
'Expansion', '2022-10-13',
'2025-04-01'),
(18, '2025 Tariff
Shock', 'Shock', '2025-04-02', '2025-04-09'),
(19, '2025-26
Recovery Rally',
'Expansion', '2025-04-10',
'2026-02-27'),
(20, '2026
Intensive Iran Bombing',
'Shock', '2026-03-02',
'2026-04-07'),
(21, '2026
Post-Bombing Recovery (open)','Expansion',
'2026-04-08', '2026-07-31');
go
-- Sanity checks -- both should come back clean before
trusting this table
-- for anything downstream.
-- 1. Row count should be 21.
select count(*) as row_count from
dbo.market_regime_ledger;
-- 2. No gaps or overlaps: for every era_id N > 1, its
start_date should be
-- exactly one
day after era_id N-1's end_date. This returns 0 rows if
-- the ledger is
fully contiguous -- expect exactly ONE row back right
-- now (era_id
20, gap_days = 3), the known open item noted above.
select
curr.era_id,
curr.era_name,
prev.end_date as
prev_end_date,
curr.start_date
as curr_start_date,
datediff(day,
prev.end_date, curr.start_date) as gap_days
from dbo.market_regime_ledger curr
join dbo.market_regime_ledger prev
on curr.era_id =
prev.era_id + 1
where datediff(day, prev.end_date, curr.start_date)
<> 1;
go
Appendix A.2: Shared Trading Dates Across Tickers
dbo.all_tickers_shared_dates isolates the trading dates
common to whichever tickers were active as of that date, which the metrics
script joins on to keep every era's day count fair across tickers with
different start dates.
use SecurityTradingAnalytics
go
-- create table to hold shared trading dates across
active tickers
if object_id('dbo.all_tickers_shared_dates', 'u') is not
null
drop table
dbo.all_tickers_shared_dates;
create table dbo.all_tickers_shared_dates (
[date] date not
null primary key
);
go
-- populate table dynamically based on active ticker
counts per era
with ticker_bounds as (
-- step 1:
identify the first trading date for each ticker string
select
ticker,
min([date])
as first_date
from
dbo.stockhistory_clean_gs
group by ticker
),
expected_counts as (
-- step 2:
determine how many tickers were active on each calendar date
select
d.[date],
count(b.ticker) as expected_ticker_count
from (select
distinct [date] from dbo.stockhistory_clean_gs) d
inner join
ticker_bounds b
on d.[date]
>= b.first_date
group by
d.[date]
)
-- step 3: insert dates where actual distinct ticker
count matches expected count
insert into dbo.all_tickers_shared_dates ([date])
select g.[date]
from dbo.stockhistory_clean_gs g
inner join expected_counts e
on g.[date] =
e.[date]
group by g.[date], e.expected_ticker_count
having count(distinct g.ticker) =
e.expected_ticker_count;
go
Appendix A.3: Section 1 — Validation Across All Tickers
This section's script pools all six tickers to check the
regime labels two ways: whether each era's price direction matches what its
regime implies, and how each regime's typical return and drawdown compare.
use SecurityTradingAnalytics
go
--
============================================================================
-- "Validating Market Regimes Across Six
Tickers" -- Section 1 (all-ticker
-- validation) appendix script
--
============================================================================
-- ASSUMPTION: this queries dbo.regime_ticker_metrics,
one row per
-- (era_id, ticker), with columns: era_id, era_name,
regime, ticker,
-- trading_days, era_start_date, era_end_date,
start_close, end_close,
-- overall_pct_return, annualized_pct_return,
max_drawdown_pct -- the shape
-- produced by regime_ticker_metrics.sql. If the actual
table/view has a
-- different name, that's the only line each query below
needs changed.
--
-- ERA 21 EXCLUDED: both queries filter out era_id 21.
Its end date
-- (2026-07-31) only marks where price data collection
currently stops, not
-- a real conclusion to the era the way every other era's
end date does --
-- the overview section's table already tells the reader
era 21 is open and
-- why, so the validation queries work only with the 20
closed eras rather
-- than mixing a settled 20-era result with one
provisional, still-moving
-- data point.
--
-- Two result sets, both pooling all six tickers (CAT,
JPM, LLY, MSFT, NVDA,
-- SHW -- MSFT and NVDA enter partway through the ledger,
so early eras are a
-- 4- or 5-ticker comparison, not 6):
--
-- 1. Magnitude
vs. velocity, by Regime -- MEDIAN (not AVG; ~20 eras per
-- Regime is a
small, outlier-prone sample, same reasoning as the rest
-- of this
analysis) of overall_pct_return and max_drawdown_pct (the
-- aggregate,
duration-capped view of a regime) alongside a plain
-- linear
daily rate and annualized_pct_return / CAGR (the
--
duration-normalized view). SQL Server's PERCENTILE_CONT is a window
-- function,
not a GROUP BY aggregate, so it needs
-- OVER
(PARTITION BY regime) plus SELECT DISTINCT to collapse to one
-- row per
Regime.
--
-- 2.
Sign-mismatch summary -- does overall_pct_return's sign match what
-- each Regime
implies (positive for Expansion; negative for
-- Contraction
and Shock)? max_drawdown_pct is deliberately excluded
-- from this
check: drawdown is structurally <= 0 for every era
-- regardless
of Regime, so "wrong sign" isn't a meaningful test for
-- that
column. NOTE: with only 26-56 era-ticker rows per regime and
-- mismatch
counts of just 3, 4, and 6, these percentages rest on very
-- small
absolute counts -- same small-sample caution as the reason
-- medians are
used above, not averages. Read this as a suggestive
-- pattern
worth further backtesting on other tickers and longer
-- timeframes,
not a settled conclusion the way the medians above are.
--
============================================================================
-- ---- Result set 1: magnitude (aggregate) vs. velocity
(rate), by Regime --
-- The DISTINCT lives inside this CTE; the ORDER BY
happens on the plain
-- SELECT * outside it, where SQL Server's "ORDER BY
items must appear in
-- the select list" restriction on SELECT DISTINCT
no longer applies.
with regime_medians as (
select distinct
regime,
percentile_cont(0.5)
within group (order by overall_pct_return)
over
(partition by regime) as median_overall_pct_return,
percentile_cont(0.5) within group (order by max_drawdown_pct)
over
(partition by regime) as median_max_drawdown_pct,
percentile_cont(0.5) within group (order by overall_pct_return /
trading_days)
over
(partition by regime) as median_daily_pct_rate,
percentile_cont(0.5) within group (order by annualized_pct_return)
over
(partition by regime) as median_annualized_pct_return
from
dbo.regime_ticker_metrics
where era_id
<> 21 -- exclude the still-open
era; see header note
)
select *
from regime_medians
order by case regime when 'Expansion' then 1 when
'Contraction' then 2 when 'Shock' then 3 end;
-- ---- Result set 2: sign-mismatch summary, all six
tickers pooled ----------
with flagged as (
select
*,
case when
regime = 'Expansion' then 1 else 0 end as expected_positive,
case when
overall_pct_return > 0 then 1 else 0 end as actual_positive
from
dbo.regime_ticker_metrics
where era_id
<> 21 -- exclude the still-open
era; see header note
)
select
regime,
count(*) as
era_ticker_rows,
sum(case when
expected_positive <> actual_positive then 1 else 0 end) as mismatches,
cast(100.0 *
sum(case when expected_positive <> actual_positive then 1 else 0 end)
/ count(*)
as decimal(5,1)) as mismatch_pct
from flagged
group by regime
order by case regime when 'Expansion' then 1 when
'Contraction' then 2 when 'Shock' then 3 end;
go
Appendix A.4: Section 2 — Validation By Individual Ticker
This section's script re-runs the same checks one ticker at
a time, so a reader can see how any single ticker compares to the six-ticker
pooled pattern from Section 1.
use SecurityTradingAnalytics
go
--
============================================================================
-- "Validating Market Regimes Across Six
Tickers" -- Section 2 (per-ticker
-- validation) appendix script
--
============================================================================
-- Same source assumption as the section 1 script:
dbo.regime_ticker_metrics,
-- one row per (era_id, ticker). Adjust the FROM clause
in both queries below
-- if the real object has a different name.
--
-- ERA 21 EXCLUDED: same reasoning as the section 1
script -- era 21's end
-- date only marks where price data collection currently
stops, not a real
-- conclusion to the era, so it's left out of both result
sets here too.
--
-- Set @ticker to any of CAT, JPM, LLY, MSFT, NVDA, SHW
and re-run. (MSFT and
-- NVDA only have rows from the era each ticker's own
price history begins --
-- era 4 for MSFT, era 7 for NVDA -- so fewer than 20
rows come back for
-- those two.)
--
-- Two result sets:
-- 1. This
ticker's own era-by-era results, with the same sign-mismatch
-- flag used
in the section 1 pooled summary, so a reader can see
-- exactly
which eras this ticker's return direction disagreed with
-- its Regime.
-- 2. This
ticker's median overall_pct_return and max_drawdown_pct by
-- Regime, set
side by side against the all-six-ticker pooled medians
-- from
section 1 -- so a reader can see at a glance whether this
-- ticker runs
milder, harsher, or in line with the pooled pattern.
--
============================================================================
declare @ticker varchar(20) = 'LLY'; -- <-- change this and re-run
-- ---- Result set 1: this ticker's era-by-era results,
flagged -------------
select
era_id,
era_name,
regime,
trading_days,
overall_pct_return,
annualized_pct_return,
max_drawdown_pct,
case
when (regime =
'Expansion' and overall_pct_return <= 0)
or (regime
<> 'Expansion' and overall_pct_return > 0)
then 'mismatch'
else ''
end as sign_check
from dbo.regime_ticker_metrics
where ticker = @ticker
and era_id
<> 21 -- exclude the still-open
era; see header note
order by era_id;
-- ---- Result set 2: this ticker vs. the six-ticker
pooled median, by Regime
with pooled as (
select distinct
regime,
percentile_cont(0.5) within group (order by overall_pct_return)
over
(partition by regime) as pooled_median_overall_pct_return,
percentile_cont(0.5) within group (order by max_drawdown_pct)
over
(partition by regime) as pooled_median_max_drawdown_pct
from
dbo.regime_ticker_metrics
where era_id
<> 21 -- exclude the still-open
era; see header note
),
this_ticker as (
select distinct
regime,
percentile_cont(0.5) within group (order by overall_pct_return)
over
(partition by regime) as ticker_median_overall_pct_return,
percentile_cont(0.5) within group (order by max_drawdown_pct)
over
(partition by regime) as ticker_median_max_drawdown_pct
from dbo.regime_ticker_metrics
where ticker =
@ticker
and era_id
<> 21 -- exclude the still-open
era; see header note
)
select
p.regime,
t.ticker_median_overall_pct_return,
p.pooled_median_overall_pct_return,
t.ticker_median_max_drawdown_pct,
p.pooled_median_max_drawdown_pct
from pooled p
join this_ticker t on t.regime = p.regime
order by case p.regime when 'Expansion' then 1 when
'Contraction' then 2 when 'Shock' then 3 end;
go
Comments
Post a Comment