DEV Community

Kristi for J.Labs

Posted on

Regime Switching Models for Adaptive Trading Strategies

A practical framework for building timeframe-specific models that respond dynamically to shifting market conditions.

Most trading systems break at the worst possible moment, when market conditions shift. Not because the math is wrong, but because a model trained on one regime is structurally blind to another.

This article presents the Adaptive Regime Framework: a production-oriented architecture for detecting and responding to changing market conditions across multiple timeframes. We'll walk through the model toolkit, the data layers powering each timeframe, and the cross-timeframe alignment mechanism that ties it all together.
At the end, we include a working Python script that pulls real historical price data from the JungleTrade API.

Why the universal regime models fail

One of the most common mistakes in quantitative trading is attempting to build a single regime model and apply it across all timeframes.

The intuition is understandable:** One model. One signal. One source of truth.**

In practice, this approach fails because the drivers of market behavior change completely as you move across the time frame hierarchy.

At the tick level, market behavior is dominated by:

  • Order book imbalance
  • Liquidity depletion
  • Spread dynamics
  • Trade arrival rates
  • Informed vs. uninformed flow

At the weekly or monthly level, regimes are governed by:

  • Central bank policy
  • Credit conditions
  • Inflation expectations
  • Liquidity cycles
  • Macroeconomic narratives

A model designed for one environment becomes structurally blind to the dynamics of another.

The Adaptive Regime Framework solves this problem through a layered architecture where:

  • Each timeframe receives its own regime model
  • Each model uses time frame-specific feature sets
  • Each layer is powered by dedicated Jungletrade market intelligence products
  • All layers are synthesized through cross-timeframe alignment

The JungleTrade Market Intelligence Stack

For the purpose of the article, we are using JungleTrade market intelligence infrastructure.

Each JungleTrade product contributes a different layer of market awareness.

Spot Order Book — High-frequency order book liquidity and depth analytics data that combines multiple centralized exchanges.

Spot Liquidity Walls — High-impact liquidity walls and significant support/resistance price levels.

Crypto Spot Prices Flow — Real-time cryptocurrency pricing

FX Exchange Rates — Global currency pressure and macro direction

News Sentiment (MENTIS) — AI-powered market sentiment intelligence analyzing financial news, media narratives, and market commentary in real time.

Market Condition Model (OMNIS) — AI-powered market condition model aggregating weighted financial, macroeconomic, and geopolitical news sentiment over the past 6 hours using impact scoring and time-decay analysis.

The Core Model Toolkit

Rather than relying on a single statistical methodology, the system uses different models depending on the behavioral characteristics of each timeframe.

1. Hidden Markov Models (HMM)

HMMs assume the market transitions between a finite number of hidden states that are not directly observable but produce observable outputs, such as:

  • Returns
  • Volatility
  • Volume
  • Liquidity imbalance
  • Spread behavior

Best suited for: Identifying regimes purely from price and volume time series, across any timeframe. The “hidden” aspect is a strength here, you are not imposing regime labels, you are discovering them from the data.

Key advantage: Regimes are discovered directly from the data rather than imposing predefined labels.

2. Markov-Switching Autoregression (MS-AR)

MS-AR models extend HMMs by allowing the autoregressive structure of returns to switch between regimes.

This enables the model to capture:

  • Momentum persistence
  • Mean reversion
  • Volatility clustering
  • Regime-dependent autocorrelation

Best suited for: Daily and weekly timeframe analysis where autocorrelation structure is meaningful, and regimes tend to persist for multiple periods.

3. Threshold Models (SETAR / STAR)

Threshold models switch regimes when observable market variables cross predefined boundaries.

Examples include:

  • Liquidity imbalance thresholds
  • Volatility expansion
  • Hurst exponent transitions
  • News sentiment pressure scores
  • VIX regime boundaries

These models are highly interpretable and suitable for structural regime transitions.

Best suited for: Structural regime transitions.

4. Hybrid Ensemble Models

Modern production systems typically combine statistical regime detection (HMM or MS-AR) with supervised machine learning (gradient boosted trees, LSTM networks) trained on labeled historical regimes. The statistical model provides the regime probability framework; the ML layer sharpens it using a richer feature set.

The model may combine:

  • Statistical regime models
  • Machine learning classifiers
  • Liquidity intelligence
  • News sentiment analytics
  • Cross-timeframe alignment

Best suited for: Combined approach where Statistical models establish regime probabilities and the Machine learning layers refine those probabilities using richer feature sets.

JungleTrade Market Microstructure Regime Model

1. Tick / Sub-Minute — Microstructure Regime Models

At the tick level, regime detection is entirely an order flow problem. Price formation here is dominated by the interaction between informed and uninformed participants, and the regime shifts occur in seconds , well before they become visible on any candlestick chart.

This layer is powered primarily by:

  • Spot Order Book
  • Spot Liquidity Walls
  • Crypto Spot Prices Flow

The key objective is to identify:

  • Informed directional flow
  • Liquidity exhaustion
  • Spread instability
  • Liquidity crises
  • Execution risk

The three states to model at this level:

Тick-level microstructure regime states: Information-Driven, Noise-Driven, and Liquidity Crisis — with key signals and strategy responses
Feature inputs:

  • Top-level order book imbalance
  • Bid/ask liquidity concentration
  • Liquidity wall formation
  • Spread Z-score expansion
  • Trade arrival acceleration
  • Real-time depth depletion analysis

Model architecture:

  • Spot Order Book provides the raw liquidity state.
  • Spot Liquidity Walls identifies significant liquidity concentrations and support/resistance structures.
  • Crypto Spot Prices Flow provides real-time pricing and spread normalization.
  • The model continuously updates regime probabilities on every new market update.
  • Latency becomes a critical engineering constraint — a correct model that reacts too slowly is functionally useless at the microstructure level.

2. 1-Minute to 5-Minute — Intraday Momentum Regime Models

At the 1–5 minute level, regimes reflect the interplay of session structure, news impact, and institutional order absorption. A critical insight from our microstructure analysis is that markets display strong intraday seasonality — the 9:30–10:00 am regime is structurally different from the 12:00–2:00 pm regime, even if the raw price action looks similar on both.

At the intraday timeframe, market behavior begins reflecting:

  • Session structure
  • Institutional order absorption
  • News-driven volatility
  • Liquidity concentration shifts

This model layer integrates:

  • Crypto Spot Prices Flow
  • Spot Order Book
  • Spot Liquidity Walls
  • News Sentiment (MENTIS)

The four states to model at this level:

Intraday momentum regime states: Opening Momentum, Midday Chop, Power Hour Trend, and Event-Driven Spike — with key signals and strategy responses
Feature inputs:

  • Session-normalized volatility
  • VWAP deviation
  • Volume acceleration
  • Liquidity imbalance persistence
  • News sentiment pressure score
  • Intraday liquidity concentration changes

Model architecture:

News Sentiment (MENTIS) becomes particularly important at this layer.

The model evaluates how recent financial, macroeconomic, and geopolitical narratives influence intraday liquidity conditions.

AI-driven sentiment scoring is combined with order book structure to distinguish genuine directional expansion from temporary volatility noise.

3. 15-Minute to 1-Hour — Intraday Structural Regime Models

At this timeframe, market microstructure begins to smooth out. What remains are structural elements: support and resistance zones, VWAP bands, hourly pivot levels, and the order block structures that institutional participants defend. As noted in our previous article, institutional order flow consistently respects hourly levels, making this the most reliable timeframe for identifying intraday inflection points.

The dominant drivers become:

  • Structural liquidity zones
  • Liquidity-based support and resistance
  • Compression and breakout structures
  • Institutional participation

This layer is primarily powered by:

  • Crypto Spot Prices Flow
  • Spot Liquidity Walls
  • CryptoQuote
  • Market Condition Model (OMNIS)
  • Regime Persistence Index (RPI)

The four states to model at this level:

1H structural regime states: Trend Continuation, Compression/Range, Breakout/Expansion, and Distribution/Accumulation — with key signals and strategy responses
Feature inputs:

  • Liquidity-based support/resistance strength
  • Hurst exponent regime classification
  • ATR expansion ratio
  • Structural breakout detection
  • Liquidity wall absorption
  • Volume-weighted order flow direction

Model architecture:

Spot Liquidity Walls becomes the dominant intelligence layer at this timeframe.

Rather than treating support and resistance as static price lines, the framework evaluates them dynamically using:

  • Liquidity concentrations
  • Order book pressure
  • Depth persistence
  • Liquidity wall formation
  • Structural breakout probability

This creates adaptive support/resistance detection rather than traditional indicator-based levels.

The Market Condition Model (OMNIS) provides the necessary information about the news feed impact on human reactions and the transition of these reactions to the market behaviour.

The Regime Persistence Index helps us identify the transition probability between different market regimes.

4. 4-Hour — Swing Regime Models

The 4-hour chart is the canonical timeframe for swing trading. Regimes here typically persist 1–3 trading days, making them actionable for positions held from several hours to several days. At this resolution, daily fundamentals begin to seep into price, but short-term noise is still substantially filtered.

At this resolution:

  • Short-term noise is filtered
  • Structural trends become clearer
  • Macro influence begins interacting with technical behavior

This layer combines:

  • Crypto Spot Prices Flow
  • FX Exchange Rates
  • Market Condition Model (OMNIS)
  • Structural Regime Engine (SRE)
  • Inflation Rates

The four states to model at this level:

4H swing regime states: Trending Bull, Trending Bear, Volatile Range, and Quiet Compression — with key signals and strategy responses
Feature inputs:

  • EMA structural alignment
  • Momentum persistence metrics
  • Bollinger Band compression
  • Realized volatility state
  • FX directional pressure
  • Precious metals defensive flow behavior
  • Macro sentiment pressure
  • Regime change probability

Model architecture:

At this layer, the framework begins incorporating cross-market intelligence.

  • Crypto Spot Prices Flow
  • FX Exchange Rates provide macro currency pressure.
  • Structural Regime Engine provides the transition probabilities between different regimes and cycles.
  • Market Condition Model (OMNIS) — provides the impact of the news flow on the market conditions and regimes and adds an additional narrative context influencing medium-term directional persistence.
  • Inflation Rates provide additional information on the market sentiment structure. Usually, people are protecting their funds from high inflation risk.
  • Structural Regime Engine (SRE) — Detects the possible regime change.

5. Daily — Multi-Day Regime Models

At the daily timeframe, fundamental regime drivers take center stage: central bank policy, earnings cycles, credit conditions, and cross-asset correlations. Daily regimes tend to persist for weeks, and their transitions — while often visible in retrospect — carry substantial uncertainty in real time. This demands a more conservative model design with higher confidence thresholds before acting.

This layer is powered by:

  • Market Condition Model (OMNIS)
  • Regime Persistence Index (RPI)
  • Structural Regime Engine (SRE)
  • Crypto Spot Prices Flow
  • FX Exchange Rates
  • Spot Liquidity Walls
  • Macro Data

The four states to model at this level:

Daily macro regime states: Risk-On/Bull, Risk-Off/Defensive, High-Volatility/Crisis, and Stagflationary — with key signals and strategy responses
Feature inputs:

  • FX directional strength
  • Cross-asset correlation stress
  • News sentiment pressure
  • Volatility regime shifts
  • Macro narrative persistence

Model architecture:

Regime Persistence Index (RPI), Market Condition Model (OMNIS) and Structural Regime Engine (SRE) become a central driver at this layer.

The model aggregates:

  • Financial news
  • Macroeconomic releases
  • Geopolitical developments
  • Cross-market narratives

This creates a continuously updating market condition engine capable of measuring directional information pressure in real time.

Cross-Timeframe Alignment — The Cascade Framework

The most powerful feature of the Adaptive Regime Framework is cross-timeframe alignment.

Higher timeframe regimes establish the structural context for lower timeframe execution.

A strong lower timeframe signal that conflicts with the dominant higher timeframe regime is treated with skepticism and reduced sizing.

The Cascade Hierarchy

Daily Market Condition Regime ↓ 4H Swing Regime ↓ 1H Structural Liquidity Regime ↓ Intraday Liquidity Execution ↓ Tick-Level Microstructure Execution

The Alignment Score

The framework computes a weighted alignment score across all active regime layers.

Higher timeframe models receive larger weighting because they define broader structural conditions.

Strong alignment across multiple timeframes allows:

  • Increased position size
  • Higher conviction
  • Wider stop placement
  • More aggressive trend participation

Regime disagreement automatically reduces:

  • Position sizing
  • Trade frequency
  • Directional conviction

This prevents one of the most common trading failures:

Taking strong short-term signals directly against dominant macro conditions.

The Model Development Workflow

Building production-grade adaptive systems requires a disciplined development process.

Step 1: Regime Labeling

Historical regimes are initially identified using:

  • Volatility clustering
  • Trend persistence
  • Liquidity conditions
  • Cross-asset relationships
  • News sentiment pressure

Step 2: Feature Engineering

The framework extracts normalized and stationary features from:

  • Market depth
  • Liquidity concentration
  • Cross-asset pricing
  • Volatility structure
  • AI-driven sentiment analysis

Step 3: Model Training and Selection

Candidate models are evaluated based on:

  • Out-of-sample accuracy
  • Transition stability
  • False positive rate
  • Detection latency
  • Regime persistence

Walk-forward validation is mandatory.

Static in-sample validation is insufficient for regime systems.

Step 4: Strategy Integration

Every detected regime maps directly into:

  • Position sizing
  • Entry aggressiveness
  • Stop-loss logic
  • Profit-taking methodology
  • Risk exposure

The framework adapts trading behavior dynamically rather than applying static rules across all market conditions.

Step 5: Continuous Monitoring and Recalibration

Markets evolve continuously.

The framework, therefore includes:

  • Rolling retraining
  • Regime drift monitoring
  • Transition uncertainty alerts
  • Liquidity structure recalibration
  • Sentiment model updates

When regime confidence deteriorates, the framework automatically reduces risk.

Uncertainty itself becomes a valid signal.

Adaptive Risk Management

Regime-aware risk management differs from static approaches in one fundamental way: the risk parameters move with the market, not against it.

Position Sizing by Regime Confidence

Position sizing by regime confidence level: High, Moderate, Low/Uncertain, and Crisis Override — mapped to dominant probability thresholds and target allocation
Stop-Loss Adaptation

  • Trending regimes favor wider ATR-based stops.
  • Range regimes favor structural liquidity-based stops.
  • High-volatility environments favor time-based risk exits to avoid noise-driven liquidation.
  • Circuit Breakers

The framework automatically reduces exposure when:

  • Regime uncertainty remains elevated
  • Liquidity deteriorates sharply
  • Cross-asset volatility spikes
  • News sentiment pressure becomes unstable

This creates a system designed not only to pursue returns but also to survive regime transitions.

Working Code: Regime Detection Starter with JungleTrade API

The following script fetches real historical price data from the JungleTrade API.

You'll Need an API key. Contact us at support@jungletrade.ai for a test key. Full API documentation at api.jungletrade.ai/api-docs

import numpy as np
import scipy.stats as sp
import pandas as pd
import matplotlib.pyplot as plt
import requests
import msvcrt

JUNGLETRADE_URL = "https://jungletrade.ai/",
JUNGLETRADE_DOCS = "https://api.jungletrade.ai/api-docs/",
JUNGLETRADE_SUPPORT = "support@jungletrade.ai",
JUNGLETRADE_INFO = "info@jungletrade.ai"
JUNGLETRADE_PRICE_ENDPOINT = "https://api.jungletrade.ai/api/v1/data/crypto-spot-prices-flow/historical"
JUNGLETRADE_PAIRS_ENDPOINT = "https://api.jungletrade.ai/api/v1/pairs"
# Contact us at support@jungletrade.ai for test API key if you don't have one. Do not share your API key publicly.
JUNGLE_API_KEY = ""
MIN_BUCKET_SAMPLES = 10


def print_test_api_key_hint() -> None:
    print("Request a test API key at support@jungletrade.ai so you can test the script.")


def prompt_api_key(configured_api_key: str) -> str:
    if configured_api_key:
        return configured_api_key.strip()

    print("An API key is required to access Jungletrade endpoints.")
    print_test_api_key_hint()
    return input("Enter your Jungletrade API key: ").strip()


def make_user_choice(options):
    print("Multiple options found. Please select the correct one:")
    for i, option in enumerate(options, 1):
        print(f"{i}. {option}")

    while True:
        if msvcrt.kbhit():
            choice = msvcrt.getch().decode('utf-8')
            if choice.isdigit() and 1 <= int(choice) <= len(options):
                return options[int(choice) - 1]
            else:
                print("Invalid choice. Please enter a number corresponding to the options above.")


def prompt_quote_and_base() -> tuple[str, str]:
    default_quote = "BTC"
    default_base = "USDC"

    while True:
        quote = input(f"Enter quote asset (example/default: {default_quote}): ").strip().upper() or default_quote
        base = input(f"Enter base asset (example/default: {default_base}): ").strip().upper() or default_base

        if not quote or not base:
            print("Quote and base cannot be empty.")
            continue

        if quote == base:
            print("Quote and base must be different.")
            continue

        return quote, base


def get_sample_rate_minutes(sample_rate: str) -> int:
    try:
        delta = pd.to_timedelta(sample_rate.lower())
    except ValueError as exc:
        raise ValueError(
            "Invalid sample rate. Use a fixed interval like 15min, 30min, 1h, 4h, or 1d."
        ) from exc

    sample_rate_minutes = int(delta.total_seconds() // 60)
    if sample_rate_minutes < 1:
        raise ValueError("Sample rate must be at least 1 minute.")

    return sample_rate_minutes


def prompt_unit_count_and_sample_rate() -> tuple[str, int, str]:
    unit_options = ["MINUTE", "HOUR", "DAY", "MONTH"]
    default_unit = "DAY"
    default_unit_count = 20
    default_sample_rate = "15min"
    minutes_per_unit = {
        "MINUTE": 1,
        "HOUR": 60,
        "DAY": 24 * 60,
        "MONTH": 30 * 24 * 60,
    }
    minimum_week_minutes = 7 * 24 * 60

    while True:
        print("Choose how much raw history to download before the analysis is resampled.")
        print("Unit is the time scale returned by the API: MINUTE, HOUR, DAY, or MONTH.")
        unit = input(
            f"Enter Unit ({', '.join(unit_options)}) [default: {default_unit}]: "
        ).strip().upper() or default_unit

        if unit not in unit_options:
            print(f"Invalid Unit. Choose one of: {', '.join(unit_options)}.")
            continue

        print(
            "UnitCount is how many of those units to fetch. "
            "Examples: 30 DAY = last 30 days, 168 HOUR = last 168 hours."
        )
        unit_count_text = input(
            f"Enter UnitCount as integer [default: {default_unit_count}]: "
        ).strip()
        unit_count = default_unit_count if not unit_count_text else None

        if unit_count is None:
            if not unit_count_text.isdigit():
                print("UnitCount must be a whole number.")
                continue
            unit_count = int(unit_count_text)

        if unit_count < 1:
            print("UnitCount must be at least 1.")
            continue

        print(
            "Resample rate is the analysis interval after download. "
            "Examples: 5min, 15min, 1h, 4h, 1d. Smaller values create more points."
        )
        sample_rate = input(
            f"Enter resample rate (example/default: {default_sample_rate}): "
        ).strip() or default_sample_rate

        try:
            sample_rate_minutes = get_sample_rate_minutes(sample_rate)
        except ValueError as exc:
            print(exc)
            continue

        covered_minutes = unit_count * minutes_per_unit[unit]
        minimum_analysis_minutes = max(
            minimum_week_minutes,
            35 * sample_rate_minutes,
            4 * MIN_BUCKET_SAMPLES * sample_rate_minutes,
        )

        if covered_minutes < minimum_analysis_minutes:
            required_count = int(np.ceil(minimum_analysis_minutes / minutes_per_unit[unit]))
            print(
                "Please request enough history for the selected resample rate. "
                f"At {sample_rate}, the analysis needs at least {minimum_analysis_minutes} minutes of data. "
                f"For {unit}, UnitCount must be at least {required_count}."
            )
            continue

        return unit, unit_count, sample_rate


def fetch_piar_id(quote: str, base: str, api_key: str) -> int:

    if not api_key:
        print("API key is required to fetch pair ID.")
        print_test_api_key_hint()
        return None

    if quote == base:
        print("Quote and base cannot be the same.")
        return None

    if not quote or not base:
        print("Quote and base cannot be empty.")
        return None

    url = JUNGLETRADE_PAIRS_ENDPOINT
    headers={
      "Accept": "application/json",
      "X-Api-Key": api_key
    }
    params = {
        "SourceId": 39,
        "Quote": base,
        "Base": quote
    }
    try:
        response = requests.get(url, headers=headers, params=params)
        response.raise_for_status()
        pairs = response.json()
    except requests.exceptions.RequestException as e:
        print(f"Error fetching pair ID: {e}")

        if getattr(e.response, "status_code", None) in [401, 403]:
            print("Invalid API key. Please check your API key and try again.")
            print_test_api_key_hint()
        return None

    if not pairs.get("items"):
        print(f"No pair found for {quote}/{base}")
        return None

    # make a user choice if len of pairs is more than 1.
    if len(pairs.get("items")) > 1:
        options = [f"{item.get('base')}/{item.get('quote')} (ID: {item.get('id')})" for item in pairs.get("items")]
        selected_option = make_user_choice(options)
        selected_id = int(selected_option.split("ID: ")[1].rstrip(")"))
        return selected_id


    return(pairs.get("items")[0].get("id"))


def retrive_historical_price_data(pair_id: int, api_key: str, Unit: str = "DAY", UnitCount: int = 20)-> dict:
    if not api_key:
        print("API key is required to fetch historical price data.")
        print_test_api_key_hint()
        return None

    url = JUNGLETRADE_PRICE_ENDPOINT
    headers={
      "Accept": "application/json",
      "X-Api-Key": api_key
    }
    params = {
        "PairId": str(pair_id),
        "Unit": Unit,
        "UnitCount": str(UnitCount),
    }
    try:
        response = requests.get(url, headers=headers, params=params)
        response.raise_for_status()
        price_data = response.json()
        return price_data
    except requests.exceptions.RequestException as e:
        print(f"Error fetching price data: {e}")
        if getattr(e.response, "status_code", None) in [401, 403]:
            print("Invalid API key. Please check your API key and try again.")
            print_test_api_key_hint()
        return None


def construct_df(price_data: dict, sample_rate: str) -> pd.DataFrame:

    if len (price_data) == 0:
        print("No price data available to construct DataFrame.")
        return pd.DataFrame()

    df_temp = []
    for item in price_data:
        try:
            timestamp = item.get("timestamp")
            price = item.get("price")
            if timestamp is not None and price is not None:
                df_temp.append({"timestamp": timestamp, "price": price})
            else:
                print(f"Skipping item with missing timestamp or price: {item}")
        except Exception as e:
            print(f"Error processing item: {e}. Item: {item}")

    if not df_temp:
        print("No valid price data available to construct DataFrame.")
        return pd.DataFrame()

    # Resample data
    price_df = pd.DataFrame(df_temp)
    price_df['timestamp'] = pd.to_datetime(price_df['timestamp'], unit='ms')
    price_df.set_index('timestamp', inplace=True)
    price_df = price_df.resample(rule = sample_rate, closed='right').first()
    price_df['returns'] = price_df['price'].pct_change(fill_method=None)
    price_df.dropna(subset=['returns'], inplace=True)

    return price_df


def get_price_df_week_componets(price_df: pd.DataFrame) -> dict:

    price_df['weekday'] = price_df.index.weekday
    price_df['day_name'] = price_df.index.day_name()

    weekend_returns = price_df[price_df['weekday'] >= 5]['returns']
    weekday_returns = price_df[price_df['weekday'] <= 4]['returns']

    return {
        "Weekend Returns": weekend_returns,
        "Weekday Returns": weekday_returns
    }

def fit_student_t_pdfs(return_buckets: dict, q_lo: float = 0.005, q_hi: float = 0.995,
                       peak_fraction: float = 1e-3, min_samples: int = MIN_BUCKET_SAMPLES) -> dict:
    cleaned_buckets = {}
    fits = {}

    for name, series in return_buckets.items():
        data = np.asarray(series, dtype=float)
        data = data[np.isfinite(data)]
        if len(data) < min_samples:
            print(f"Skipping {name}: need at least {min_samples} points, got {len(data)}.")
            continue
        cleaned_buckets[name] = data
        fits[name] = sp.t.fit(data)

    if not fits:
        raise ValueError("No bucket has enough data to fit a Student-t distribution.")

    lows = []
    highs = []
    for df_, loc_, scale_ in fits.values():
        lo, hi = sp.t.ppf([q_lo, q_hi], df_, loc=loc_, scale=scale_)
        if np.isfinite(lo) and np.isfinite(hi) and lo < hi:
            lows.append(lo)
            highs.append(hi)

    if lows and highs:
        low = max(lows)
        high = min(highs)
    else:
        all_data = np.concatenate(list(cleaned_buckets.values()))
        low, high = np.percentile(all_data, [q_lo * 100, q_hi * 100])

    if not np.isfinite(low) or not np.isfinite(high) or low >= high:
        all_data = np.concatenate(list(cleaned_buckets.values()))
        low, high = np.percentile(all_data, [q_lo * 100, q_hi * 100])

    x = np.linspace(low, high, 1200)
    eps = 1e-15
    curves = {}

    for name, (df_, loc_, scale_) in fits.items():
        pdf = sp.t.pdf(x, df_, loc=loc_, scale=scale_)
        peak = max(pdf.max(), eps)
        mask = pdf > (peak_fraction * peak)
        curves[name] = {
            "x": x[mask],
            "pdf": pdf[mask],
            "fit": (df_, loc_, scale_),
            "sample_size": len(cleaned_buckets[name])
        }

    return curves


def plot_pdfs(pdf_curves: dict, asset: str, title_suffix: str):
    for name, curve in pdf_curves.items():
        plt.plot(curve["x"], curve["pdf"], label=name)

    plt.title(f'{asset} Returns Distribution: {title_suffix}')
    plt.xlabel('Returns')
    plt.ylabel('Density')
    plt.legend()
    plt.grid()
    plt.show()


def time_of_day_analysis(price_data: pd.DataFrame)-> dict:
    # --- Split into day parts ---
    morning_returns   = price_data.between_time("06:00", "11:59")["returns"]
    afternoon_returns = price_data.between_time("12:00", "17:59")["returns"]
    evening_returns   = price_data.between_time("18:00", "23:59")["returns"]
    night_returns     = price_data.between_time("00:00", "05:59")["returns"]

    time_buckets = {
        "Morning (06-12)":   morning_returns,
        "Afternoon (12-18)": afternoon_returns,
        "Evening (18-24)":   evening_returns,
        "Night (00-06)":     night_returns,
    }
    return time_buckets

def main():
    api_key = prompt_api_key(JUNGLE_API_KEY)
    if not api_key:
        print("Cannot continue without an API key.")
        print_test_api_key_hint()
        return

    quote, base = prompt_quote_and_base()
    unit, unit_count, sample_rate = prompt_unit_count_and_sample_rate()
    pair_id = fetch_piar_id(quote=quote, base=base, api_key=api_key)
    if pair_id is None:
        return

    price_data = retrive_historical_price_data(
        pair_id=pair_id,
        api_key=api_key,
        Unit=unit,
        UnitCount=unit_count
    )
    if not price_data:
        return

    price_df = construct_df(price_data=price_data, sample_rate=sample_rate)

    week_buckets = get_price_df_week_componets(price_df=price_df)
    week_pdfs = fit_student_t_pdfs(week_buckets)
    plot_pdfs(week_pdfs, asset=f'{quote}/{base}', title_suffix='Weekday vs Weekend')

    time_buckets = time_of_day_analysis(price_data=price_df)
    time_pdfs = fit_student_t_pdfs(time_buckets)
    plot_pdfs(time_pdfs, asset=f'{quote}/{base}', title_suffix='Time of Day')

if __name__ == "__main__":
    main()

Enter fullscreen mode Exit fullscreen mode

What this script does

Fetches historical data from the JungleTrade API for any crypto pair you choose (e.g. BTC/USDC).

Conclusion

The framework presented here is built around one core idea: translate the theoretical understanding of regime changes into a systematic, repeatable model-building process.
Each timeframe gets its own model, tuned to the drivers that actually govern behavior at that resolution. Those models are connected through the cross-timeframe alignment cascade, so lower-timeframe signals are always evaluated in the context of the macro regime. The entire system operates with explicit uncertainty quantification — when the model doesn't know, it says so, and position sizing reflects that.

The goal is not prediction. It is regime-aware adaptation: recognize the current market state as accurately as possible, size positions proportionally to confidence, and transition gracefully when conditions change.


We build the market intelligence infrastructure behind this framework at JungleTrade. If you're working on similar problems - regime detection, order book analytics, AI-driven sentiment, we'd love to hear from you. Reach us at info@jungletrade.ai

Top comments (0)