Analyzing Financial Trends: Kalman Filtering for Gold vs Bitcoin

Wait 5 sec.

[This article was first published on DataGeeek, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.In their paper “A Synchronized Multi‑IMU Wearable System for Tracking of Joint‑Angles in Sports Motion Analysis” (arXiv:2607.26027v1), Samarasekera and colleagues set out to solve a very practical problem: how to reliably measure joint angles in dynamic sports movements using wearable IMUs. Their goal was to design a synchronized pipeline that could filter out noise, correct drift, and normalize ranges so that biomechanical signals could be trusted in real‑world conditions.Our goal is different but conceptually parallel: to apply the same pipeline logic to financial time‑series data. Instead of knee flexion angles, we analyze the comparative dynamics of Gold and Bitcoin. The challenge is similar — financial returns are noisy, prone to drift, and difficult to compare directly. By adapting their pipeline, we aim to extract latent trends, mitigate drift, normalize ranges, and highlight relative divergence between assets.The pipeline consists of four sequential stages:Indirect Kalman Filtering (IKF) – extracting latent trends from noisy log returns.State equation:xt=F⋅xt−1+wt,wt∼N(0,Q)Observation equation:yt=H⋅xt+vt,vt∼N(0,R)IKF separates signal from noise, with Q and R learned via maximum likelihood.Drift Mitigation via High‑Pass Filtering – removing low‑frequency bias.High‑pass operator:xtHP=xt−1N∑i=1Nxt−iThis ensures long‑term drift does not dominate short‑term dynamics.Range Normalization – mapping filtered values into a comparable scale.Normalization function:zt=xtHP−min⁡(xHP)max⁡(xHP)−min⁡(xHP)⋅100This allows Gold and Bitcoin to be compared on the same 0–100 scale.Relative Notation (Directional Divergence) – highlighting when assets move in opposite directions.Divergence indicator:Dt=sign(rtGold)≠sign(rtBTC)If true, shaded regions show divergence; if false, alignment.Why This MattersFor the biomechanics researchers, the pipeline meant trustworthy joint‑angle tracking.For us, the same architecture means trustworthy comparative trend analysis between financial assets.In both cases, the pipeline’s strength lies in its modularity: IKF for latent signal extraction, high‑pass filtering for drift control, normalization for comparability, and relative notation for divergence detection.✦ R Packages in Our PipelineTo make this pipeline reproducible and transparent, we rely on a set of R packages. Each package plays a specific role in the architecture:tidyversePurpose: Data manipulation and wrangling.Functions: mutate, rename, inner_join, drop_na.Why: Provides a clean grammar for transforming raw financial data into structured time‑series.tidyquantPurpose: Financial data acquisition and transformation.Functions: tq_get (download asset prices), tq_transmute (compute log returns).Why: Bridges tidyverse with financial APIs, enabling reproducible market data pipelines.KFASPurpose: State‑space modeling and Kalman filtering.Functions: SSModel (define state‑space model), fitSSM (MLE parameter estimation), KFS (Kalman smoothing).Why: Implements the mathematical backbone of IKF, separating latent trends from noise.zooPurpose: Rolling window operations.Functions: rollmean.Why: Used for high‑pass filtering by subtracting rolling averages, mitigating drift.scalesPurpose: Normalization and rescaling.Functions: rescale.Why: Maps filtered trends into a 0–100 range, enabling comparability across assets.ggbraidPurpose: Divergence visualization.Functions: geom_braid.Why: Highlights periods of divergence between Gold and Bitcoin with shaded regions.ggplot2 (via tidyverse)Purpose: Visualization.Functions: geom_line, labs, theme_classic.Why: Provides the plotting framework for presenting normalized high‑pass Kalman trends.library(tidyverse)library(tidyquant)library(KFAS)library(ggbraid)# --- Step 1: Log returns ---gold % tq_transmute(select = close, mutate_fun = periodReturn, period = "daily", type = "log")btc % tq_transmute(select = close, mutate_fun = periodReturn, period = "daily", type = "log")df % rename(ret_gold = daily.returns) %>% inner_join(btc %>% rename(ret_btc = daily.returns), by="date") %>% drop_na()# --- Step 2: IKF (Kalman filter) ---model_gold