Back to Projects

Financial Analytics Pipeline on Databricks

SAS has been the standard for market analytics in financial institutions for decades. Moving to a modern data platform means replacing both the infrastructure and the code. This is a personal project — built outside work on real data: a Databricks medallion pipeline on 15 years of US and European market data, and a tool that converts SAS code to PySpark automatically.

The pipeline covers five data series: S&P 500, Euro Stoxx 50, VIX (a measure of expected US stock market volatility), US Federal Funds Rate, and ECB (European Central Bank) Deposit Facility Rate. These run through three Delta Lake layers — Bronze, Silver, Gold. The ECB rate turned negative in 2014 and stayed there until 2022 — a regime the US never entered, which appears clearly in the regime classifications. The converter handles the code side: SAS in, PySpark out.

Live dashboard ↗  ·  GitHub ↗  ·  Lakehouse overview ↗  ·  SAS migration ↗  ·  SAS converter ↗

The dashboard is hosted on Streamlit Community Cloud and may be sleeping after inactivity — clicking will wake it up within a few seconds.

Medallion Architecture

Data flows through three Delta Lake layers, each with a distinct role:

The separation matters because interest rates are monthly while equity prices are daily — a simple date join leaves the rate column empty on most trading days. Silver solves this in a single tested transformation, and everything built on top reads from the same aligned source. Bronze preserves source data exactly as received; if a cleaning rule changes, the full history can be re-derived without calling the original APIs again. In SAS, ingestion, cleaning, and analytics typically share one script with no clear boundary; the medallion architecture makes each step explicit and independently testable.

Databricks Catalog showing all seven Delta tables: five Bronze tables, silver_market, and gold_analytics
All seven Delta tables in the Databricks Catalog after a full pipeline run.

Joining Monthly Rates to Daily Prices

The Silver layer handles this alignment with two window functions: a forward-fill carries each rate reading forward through subsequent trading days, and a backward-fill covers the period before the first rate observation.

Forward- and backward-fill — Silver layer (PySpark)
fill_window = Window.orderBy("date").rowsBetween(
    Window.unboundedPreceding, Window.currentRow
)
back_window = Window.orderBy("date").rowsBetween(
    Window.currentRow, Window.unboundedFollowing
)

joined = daily_df.join(monthly_df, on="date", how="left")
filled = (
    joined
    .withColumn("fed_rate",
        F.last(F.col("fed_rate"), ignorenulls=True).over(fill_window))
    .withColumn("fed_rate",
        F.first(F.col("fed_rate"), ignorenulls=True).over(back_window))
)

Each trading day carries only the rate that was known at that date — no future data bleeds back.

Silver notebook output showing all quality checks passing: row count, null checks, range checks, duplicate checks for silver_market table
Quality check output after the Silver layer runs — every check passes before the pipeline proceeds to Gold.

Gold Layer

Gold computes the financial analytics: rolling realised volatility at 20-day and 60-day windows (annualised), 60-day rolling Pearson correlation between US and EU equity returns, S&P 500 % decline from its 52-week high, and rate regime classifications for both central banks. The dashboard reads these figures directly from the Gold table.

Regime classifications use threshold rules calibrated to the 2010–present rate environment. The EU classification includes a negative rate band that has no equivalent on the US side.

Gold notebook summary output showing row counts, regime distribution table, and most recent rows from gold_analytics Delta table
Gold notebook summary — regime distribution and most recent rows from the gold_analytics table.

Dashboard

Rolling volatility, correlations, and rate regime classifications run here in PySpark, reading live from the Gold Delta table on Databricks. No local data files, no intermediate exports. The app may be sleeping due to inactivity — clicking will wake it up within a few seconds.

Dashboard home page showing two sections: The Pipeline and The Migration Tool
Dashboard home page — the pipeline and the migration tool side by side.

The Analytics Dashboard opens with what the pipeline found: US and EU markets fall together in crises; the ECB held rates below zero for eight years while the US did not; 2022 saw the fastest rate rises in four decades; COVID markets recovered their pre-crash highs within 12 months. All of this comes from 15 years of real market data flowing through the Bronze → Silver → Gold pipeline, with interactive charts to explore any time period.

Analytics Dashboard showing key patterns found in 15 years of US and European market data
Analytics Dashboard: key patterns from 15 years of US and European market data, drawn from the Gold layer output.

SAS → PySpark Converter

Moving the infrastructure to Databricks is the easier half of a migration. The harder half is the code: financial institutions have decades of SAS scripts covering risk models, regulatory reports, and portfolio calculations. Rewriting them by hand takes months; this converter automates the translation. A rule engine handles the common patterns — PROC SORT, PROC MEANS, PROC SQL, DATA steps — with consistent output and no API key needed. Anything it cannot handle is passed to an LLM.

The converter has two modes. Free Edition mode converts a single SAS block, with three built-in examples and a fourth tab that shows — on a RETAIN example — what the rule engine flags versus what an LLM produces.

Enterprise mode takes a full SAS script and a YAML config file that maps library names and variable references to Databricks paths. It converts the script in one pass, scores each block by confidence, and flags anything below 85% for review. The dashboard includes a four-block example script. The first three blocks are handled by the rule engine; Block 4 contains a RETAIN statement it cannot translate, and the LLM equivalent is shown alongside. Both the converted code and a per-block review manifest are downloadable.

SAS to PySpark Converter in Free Edition mode showing three preloaded example tabs and a fourth tab demonstrating where an LLM is needed
The SAS → PySpark Converter: Free Edition mode with preloaded examples; Enterprise mode converts a full script in one pass and scores each block by confidence.

39 pytest cases cover every supported SAS construct across Free Edition and Enterprise modes — including RETAIN handling, confidence scoring, and config file support. A further 7 cover the pipeline script — credential loading, exit codes, and job status.

pytest terminal output showing all 46 tests passing — 39 SAS converter tests and 7 pipeline script tests
All 46 tests passing — 39 for the SAS converter, 7 for the pipeline script.

Free Edition and Production Databricks

This project runs on Databricks Free Edition — a free tier with standard Delta Lake and classic compute. The code is production-ready; moving to a full workspace changes how the pipeline is run and orchestrated, not the underlying data logic.

Feature Free Edition (this project) Full Databricks workspace
Data governance Hive Metastore, table-level only Unity Catalog — column-level security, data lineage, three-level naming (trading.bronze.sp500)
Pipeline definition Manual notebook execution, explicit writes, quality check function calls Delta Live Tables — declarative, @dlt.expect quality rules, auto-scaling, dependency graph inferred automatically
Ingestion API-based ingestion — yfinance and FRED API called directly from notebooks Auto Loader — incremental, schema inference, handles late-arriving files automatically
Orchestration Polling script (run_pipeline.py) Databricks Jobs — dependency graph, retry logic, alerts, schedule
Dashboard hosting Streamlit Community Cloud Databricks Apps — hosted inside the workspace, workspace authentication

The most visible change at the pipeline level is Delta Live Tables. The Bronze notebook currently writes each table explicitly and calls a quality check function. In a full workspace, both collapse into a declarative definition:

Bronze ingestion: Free Edition vs Delta Live Tables
# Free Edition — explicit write + quality check function
run_quality_checks(sp500_df, "bronze_sp500", min_rows=3000, null_cols=["close"])
sp500_df.write.format("delta").mode("overwrite").saveAsTable("bronze_sp500")

# Full Databricks — Delta Live Tables
import dlt

@dlt.table(name="bronze_sp500")
@dlt.expect_or_fail("valid_close", "close IS NOT NULL")
@dlt.expect_or_fail("sufficient_rows", "COUNT(*) > 3000")
def bronze_sp500():
    return (
        spark.read.csv(f"{DATA_DIR}/sp500.csv", header=True)
        .withColumn("date",  F.col("date").cast(DateType()))
        .withColumn("close", F.col("close").cast(DoubleType()))
        .withColumn("ingested_at", F.current_timestamp())
    )

Quality checks become @dlt.expect decorators and the write is implicit. Execution order is inferred automatically from dlt.read() calls — Silver reads from Bronze, Gold reads from Silver — so ordering and scaling are handled by the framework, not the notebook.

AI-Assisted Development

This project was built with Claude Code (Anthropic’s AI coding assistant) as a development collaborator — used for architecture decisions, the quality checks module, the converter’s rule engine design, and review throughout all three pipeline layers. The converter’s LLM fallback runs on the same Claude API — the difference is context: here it assisted with engineering decisions; there it translates SAS code across an entire migration.

References