DEV Community

Cover image for How to Use Trigonometry to Detect Regime Changes in Financial Markets
Cristian Mendoza
Cristian Mendoza

Posted on

How to Use Trigonometry to Detect Regime Changes in Financial Markets

A step-by-step guide — computed manually and automated in Python — that shows how to obtain In‑Phase (I) and Quadrature (Q) components from prices, estimate the dominant period, compute instantaneous phase, and use phase quadrants to detect regime changes.

This article explains:

  • What I (in‑phase) and Q (quadrature) components represent for price cycles.
  • How to estimate the cycle period (T) from peaks, autocorrelation, and FFT.
  • How to compute I and Q manually and with code.
  • How to compute phase using atan2 and interpret the four quadrants to detect regime changes (entry/exit timing). Includes numeric examples, ASCII diagrams and a reproducible Python script.

Key concepts

  • Centered price: P(t) = price(t) − local_mean.
  • Period T: number of samples between two consecutive peaks (max→next max) or two consecutive troughs (min→next min). Peak→trough = T/2.
  • Angular frequency ω = 2π / T.
  • I(t) = P(t) × sin(ω t) → in‑phase component (position relative to the reference cycle).
  • Q(t) = P(t) × cos(ω t) → quadrature component (90° phase-shifted, related to velocity).
  • Phase θ(t) = atan2(Q(t), I(t)) — use atan2 to obtain angles covering all four quadrants.
  • Instantaneous amplitude = sqrt(I² + Q²) ≈ |P(t)| when the assumed ω is aligned with the signal.

Example data (manual computation)

Use this simplified sample:

Day: 1 2 3 4 5 6 7 8 9 10 11 12

Price: 100 102 105 107 106 103 100 98 97 99 102 105

  1. Center the prices:

    • Mean = 102
    • P = Price − 102 → [-2, 0, 3, 5, 4, 1, -2, -4, -5, -3, 0, 3]
  2. Count indices:

    • Peak at t=4 (value +5)
    • Trough at t=9 (value −5)
    • Peak→trough = 9 − 4 = 5 samples = T/2 → T ≈ 10 samples
    • ω = 2π / 10 ≈ 0.628 rad/sample
  3. Compute I and Q manually:

    • I(t) = P(t) * sin(ω t)
    • Q(t) = P(t) * cos(ω t)

Example at t=4:

  • sin(ω·4) ≈ 0.587 → I4 ≈ 5 × 0.587 ≈ 2.94
  • cos(ω·4) ≈ −0.809 → Q4 ≈ 5 × (−0.809) ≈ −4.05 Check: I4² + Q4² ≈ 2.94² + (−4.05)² ≈ 25.04 → amplitude ≈ 5 (matches |P4|)
  1. Phase and quadrant interpretation:
  2. θ = atan2(Q, I)
  3. Quadrant interpretation for regime detection:
    • I>0, Q>0 → Quadrant I → early rise → regime switch to bullish (consider long entry)
    • I<0, Q>0 → Quadrant II → mature rise → maintain long, consider partial exit
    • I<0, Q<0 → Quadrant III → early fall → regime switch to bearish (consider short entry / exit longs)
    • I>0, Q<0 → Quadrant IV → mature fall → maintain short, prepare to cover

The (I,Q) trajectory typically rotates counterclockwise as phase advances.


Estimating the period (practical and robust)

Common approaches:

  1. Peak detection (peak→peak or trough→trough): simple and direct.
  2. Peak→trough doubling: useful when only half-cycle is visible.
  3. Autocorrelation: find the lag that maximizes correlation — robust to noise and can use median across windows.
  4. FFT: find dominant spectral peak — effective on longer windows.

Practical notes:

  • Smooth the series before peak detection (e.g., short moving average).
  • Use median of multiple peak intervals to reduce outlier influence.
  • With short windows or nonstationary signals, prefer autocorrelation or adaptive methods (e.g., rolling FFT or Hilbert-based methods).

Reproducible Python implementation

The script below:

  • centers prices,
  • estimates period via peak detection and autocorrelation,
  • computes ω, I, Q,
  • computes phase and a simple quadrant-based signal,
  • prints tabular results.
# File: compute_iq_phase_en.py
# Requirements: numpy; scipy (optional for find_peaks); matplotlib (optional for plots)
import numpy as np

def center(series):
    return series - np.nanmean(series)

def estimate_period_by_peaks(series, distance_min=3, prominence=1.0):
    """Estimate period by detecting peaks and troughs (requires scipy)."""
    try:
        from scipy.signal import find_peaks
    except Exception:
        return None
    peaks, _ = find_peaks(series, distance=distance_min, prominence=prominence)
    troughs, _ = find_peaks(-series, distance=distance_min, prominence=prominence)
    periods = []
    if len(peaks) >= 2:
        periods += list(np.diff(peaks))
    if len(troughs) >= 2:
        periods += list(np.diff(troughs))
    if periods:
        return float(np.median(periods))
    return None

def estimate_period_autocorr(series, max_lag=None):
    """Estimate period by autocorrelation: return lag with max correlation (excluding lag 0)."""
    n = len(series)
    if max_lag is None:
        max_lag = n // 2
    s = series - np.nanmean(series)
    ac = np.correlate(s, s, mode='full')[n-1:]
    # ignore lag 0
    ac[0] = 0
    lag = np.argmax(ac[1:max_lag]) + 1
    return float(lag)

def compute_iq(series, period, t0=1):
    """Compute I and Q given centered series and period. t0 is starting time index (1-based)."""
    omega = 2 * np.pi / period
    n = len(series)
    t = np.arange(t0, t0 + n)  # t = 1,2,... by default
    I = series * np.sin(omega * t)
    Q = series * np.cos(omega * t)
    return I, Q, omega

def phase_and_signal(I, Q):
    theta = np.arctan2(Q, I)
    signals = []
    for i, q in zip(I, Q):
        if i > 0 and q > 0:
            signals.append("BUY")
        elif i < 0 and q > 0:
            signals.append("HOLD_LONG")
        elif i < 0 and q < 0:
            signals.append("SELL_SHORT")
        elif i > 0 and q < 0:
            signals.append("HOLD_SHORT")
        else:
            signals.append("TRANSITION")
    return theta, signals

if __name__ == "__main__":
    prices = np.array([100,102,105,107,106,103,100,98,97,99,102,105], dtype=float)
    P = center(prices)
    period_peaks = estimate_period_by_peaks(P, distance_min=2, prominence=1.0)
    period_ac = estimate_period_autocorr(P)
    period = period_peaks if period_peaks is not None else period_ac

    print("Estimated period (peaks):", period_peaks)
    print("Estimated period (autocorr):", period_ac)
    print("Chosen period:", period)

    I, Q, omega = compute_iq(P, period, t0=1)
    theta, signals = phase_and_signal(I, Q)

    print("\n t |   P   |    I    |    Q    |  phase(deg) | signal")
    print("------------------------------------------------------")
    for idx, (p, i, q, th, s) in enumerate(zip(P, I, Q, theta, signals), start=1):
        print(f"{idx:2d} | {p:5.1f} | {i:7.2f} | {q:7.2f} | {np.degrees(th):8.1f}° | {s}")
Enter fullscreen mode Exit fullscreen mode

Notes:

  • estimate_period_by_peaks uses scipy.signal.find_peaks. If SciPy is not installed, the script falls back to the autocorrelation method.
  • t0 chooses the time indexing. To match manual calculations use t0=1.
  • For production use, compute the local mean with a rolling window and estimate period adaptively.

Visualization recommendations

  • Plot 1: centered price P(t) with peaks and troughs annotated.
  • Plot 2: phase trajectory (I(t), Q(t)) with arrows showing time direction.
  • Plot 3: phase θ(t) with vertical bands marking quadrants and suggested trading actions.

Matplotlib is adequate for these plots. For multiple assets, normalize amplitude or compare phase only.


Practical considerations and risks

  • Phase-based signals assume cyclic behavior: they can produce false signals in trending or non-cyclic regimes.
  • Noise and asymmetry bias period estimates: use smoothing and robust statistics (median).
  • Backtest thoroughly and use risk controls (position sizing, stop-loss, out-of-sample validation).
  • Hilbert transform and adaptive filters (e.g., Ehlers) can produce smoother I/Q estimates for noisy financial data but require careful parameterization.

Top comments (0)