STOXX Index Pipeline

The STOXX index pipeline ran in GCP from yfinance acquisition through Eventarc notification on 2026-04-13. Raw JSON landed in GCS, SQL Server on stoxx-vm executed the bronze, silver, and gold layers, BigQuery held the analytical replica and marts, Firestore published the serving documents, and Eventarc emitted the downstream publish event.

Each section below focuses on one operational boundary: runtime topology, deployment, validation, and the supporting artifact bundle. The execution path stays explicit so the pipeline can be audited without relying on hidden project context.

Architecture overview

The architecture section defines the key terms used throughout the note, maps the runtime topology, and separates the storage and execution boundaries that make the pipeline reproducible.

Key terms

TermDefinitionOperational role
OHLCVOpen, high, low, close, and volume observations recorded per symbol and trading date.Forms the raw market-history series staged from yfinance, landed in bronze, and historized in silver.
medallion architectureA layered data pattern that separates raw landing, cleaned historical facts, and business-facing outputs.Explains why SQL Server keeps distinct bronze, silver, and gold schemas with different contracts.
DAGDirected acyclic graph. In Airflow, it defines task order, dependency rules, and runtime behavior for a workflow.stoxx_stage_yfinance is the orchestration contract for the pipeline run.
IAPIdentity-Aware Proxy tunneling for access to private Google Cloud resources.Administrative access to stoxx-vm and stoxx-airflow uses IAP instead of public IP exposure.
break-glassEmergency administrative access reserved for recovery when the standard operator path fails.dba_break_glass is the named SQL Server recovery login used to re-establish sysadmin access safely.
CloudEventA standard event envelope that carries source, subject, type, and time metadata across eventing systems.Eventarc delivers the Firestore control-document write to the receiver service as a CloudEvent.

Runtime topology

The runtime topology shows where orchestration ends, where stateful processing happens, and where the serving publish boundary emits its downstream event.

Depicts the runtime topology from external acquisition through the Firestore control-document event boundary.

flowchart TD
    YF["yfinance<br/>remote market + fundamentals API"]
    CR1["Cloud Run Job<br/>stoxx-stage-fetch"]
    GCS["GCS Bucket<br/>gs://stoxx-stage-bucket"]
    AF["Airflow 3.2.0<br/>stoxx-airflow VM"]
    DAG["DAG<br/>stoxx_stage_yfinance"]
    CR2["Cloud Run Job<br/>stoxx-bronze-load"]
    SQLB["SQL Server on stoxx-vm<br/>bronze"]
    CRT["Cloud Run Job<br/>stoxx-transforms"]
    SQLS["SQL Server on stoxx-vm<br/>silver"]
    SQLG["SQL Server on stoxx-vm<br/>gold"]
    CRS["Cloud Run Job<br/>stoxx-serving"]
    BQR["BigQuery replica<br/>stoxx_silver / stoxx_gold"]
    BQM["BigQuery marts<br/>stoxx_marts"]
    FS["Firestore database main<br/>stoxx_indices + serving_control/current"]
    EA["Eventarc trigger<br/>stoxx-firestore-control-written"]
    RCV["Cloud Run service<br/>stoxx-firestore-eventarc-receiver"]
    LOG["Cloud Logging<br/>receiver logs"]

    AF --> DAG
    DAG --> CR1
    CR1 --> YF
    CR1 --> GCS
    DAG --> CR2
    GCS --> CR2
    CR2 --> SQLB
    DAG --> CRT
    SQLB --> CRT
    CRT --> SQLS
    CRT --> SQLG
    DAG --> CRS
    SQLG --> CRS
    CRS --> BQR
    CRS --> BQM
    CRS --> FS
    FS --> EA
    EA --> RCV
    RCV --> LOG

Processing layers

The pipeline moves the same business entities across raw, operational, analytical, and serving contracts. Each layer below has a different retention model and consumer boundary.

Raw stage in GCS

  • Stores the first durable cloud copy of the fetched yfinance payloads.
  • Writes JSON objects under gs://stoxx-stage-bucket in prefixes such as stage/, dimensions/, pulse/, and manifests/.
  • Forms the replay boundary between external acquisition and SQL ingestion.

Bronze

  • Holds the newest operational SQL snapshot with minimal reshaping.
  • Keeps ingestion logic simple and recoverable.
  • Optimizes for reliable landing, not for historical analytics.

Silver

  • Holds cleaned and historized records at business grain.
  • Gap-fills OHLCV history and upserts daily and quarterly signals.
  • Turns raw snapshots into time series that can be trusted for repeatable downstream logic.

Gold

  • Holds business-facing metrics derived from silver.
  • Computes scores, rankings, and index-level performance that products and analysts can use directly.
  • Stays inside SQL Server because the transformations are already implemented there in T-SQL and Python job wrappers.

BigQuery replica

  • Holds analytical copies of the selected SQL silver and gold tables.
  • Offloads heavier analytical reads and mart builds from the SQL VM.
  • Preserves a stable handoff from operational SQL to cloud analytics.

BigQuery marts

  • Hold dashboard-oriented read models such as mart_index_factsheet_latest and mart_constituent_screener_latest.
  • Apply the final aggregation grain and join logic needed by the serving layer.
  • Separate business consumption models from the raw analytical replica.

Firestore serving layer

  • Holds pre-shaped documents for low-latency application reads.
  • Publishes root documents and subcollections in Firestore database main.
  • Avoids runtime analytical joins by materializing the dashboard-facing view ahead of time.

Firestore control document

  • Holds the publish-complete signal for the latest serving snapshot.
  • Updates serving_control/current with publish_id, counts, and affected indexes.
  • Creates one deliberate event boundary for Eventarc instead of treating every serving-document write as a separate downstream event.

Runtime components

These runtime surfaces own orchestration, stateful persistence, serving publication, or event delivery. Each component description identifies the boundary it controls.

SQL VM stoxx-vm

  • Hosts SQL Server 2022 and the stoxx medallion database.
  • Runs in europe-west1-b on private IP 10.132.0.8.
  • Acts as the stateful operational persistence tier for bronze, silver, and gold.

Airflow VM stoxx-airflow

  • Hosts Airflow 3.2.0 in Docker Compose.
  • Runs in europe-west1-b on private IP 10.132.0.9.
  • Owns orchestration only; heavy ingestion and transformation work runs in Cloud Run jobs.

Stage bucket gs://stoxx-stage-bucket

  • Holds raw JSON landing files and manifests.
  • Uses regional storage with uniform bucket-level access.
  • Preserves deterministic replay inputs if a downstream step fails.

Cloud Run job stoxx-stage-fetch

  • Fetches market and fundamentals data from yfinance.
  • Lands JSON into GCS.
  • Last validated success ran on 2026-04-13T17:30:57Z.

Cloud Run job stoxx-bronze-load

  • Reads the staged JSON objects from GCS.
  • Loads the bronze SQL tables on stoxx-vm.
  • Last validated success ran on 2026-04-13T17:32:15Z.

Cloud Run job stoxx-transforms

  • Builds silver and gold inside SQL Server.
  • Applies the historization and scoring logic.
  • Last validated success ran on 2026-04-13T17:35:40Z.

Cloud Run job stoxx-serving

  • Replicates SQL outputs into BigQuery.
  • Builds the BigQuery marts and publishes Firestore documents.
  • Last validated success ran on 2026-04-13T18:14:04Z.

BigQuery datasets stoxx_silver, stoxx_gold, stoxx_marts

  • Hold the analytical replica and the final marts.
  • Separate scalable analytical reads from the SQL operational tier.
  • Were populated and validated during the live pipeline run documented here.

Firestore database main

  • Holds the serving documents and the control document.
  • Runs in native mode with real-time listeners available to clients.
  • Provides the document-oriented serving boundary after the analytical marts are built.

Eventarc trigger stoxx-firestore-control-written

  • Watches the Firestore control document for publish-complete writes.
  • Routes matching Firestore CloudEvents to the receiver service.
  • Acts as the first downstream observer of the serving publish boundary.

Receiver service stoxx-firestore-eventarc-receiver

  • Receives Firestore CloudEvents from Eventarc.
  • Logs the event payload into Cloud Logging.
  • Provides visible operational proof that the end-to-end publish completed.

Provisioning and deployment

Provisioning combines the verified SQL Server and Airflow bring-up with the BigQuery, Firestore, and Eventarc steps that complete the pipeline. The commands and outputs below show the reproducible deployment path; the full embedded scripts now live in 02-stoxx-index-pipeline-source-files.

Provision the SQL target on stoxx-vm

The pipeline writes into the SQL Server database stoxx, not stoxx_db. The final target keeps only the medallion schemas bronze, silver, and gold, but it preserves the VM-specific split file layout:

  • data files on /mnt/sqldata
  • log file on /mnt/sqllog

The full VM and SQL bring-up is documented in 04-sql-server-on-compute-engine. The pipeline-specific minimum is:

  1. regain safe sysadmin access
  2. create a named break-glass admin login
  3. create the stoxx_pipeline login
  4. grant the bronze and transform roles it needs
  5. validate the final stoxx database layout

SQL admin recovery has two failure paths

Two operator errors break the recovery boundary quickly:

  • Running mssql-conf -n set-sa-password while mssql-server is still active fails immediately and leaves no working admin path.
  • Writing generated passwords to instance metadata or markdown notes turns emergency credentials into standing credentials.

The recovery sequence must preserve both recoverability and credential containment.

SQL admin recovery stays bounded

Stop mssql-server, generate fresh credentials, write them only to /root/.stoxx_sql_login.env under umask 077, reset sa, restart SQL Server, and then create and verify a named sysadmin login such as dba_break_glass.

Reset sa and write a root-only credential file

Opens an IAP shell on the SQL VM, stops SQL Server, generates fresh credentials, writes the root-only environment file, and resets the sa password.

gcloud compute ssh stoxx-vm \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap
 
sudo systemctl stop mssql-server
 
umask 077
 
SA_PWD=$(python3 - <<'PY'
import secrets
print("Sa!2026" + secrets.token_hex(10))
PY
)
 
BREAK_GLASS_PWD=$(python3 - <<'PY'
import secrets
print("Bg!2026" + secrets.token_hex(10))
PY
)
 
cat > /root/.stoxx_sql_login.env <<EOF
SA_PWD=$SA_PWD
BREAK_GLASS_LOGIN=dba_break_glass
BREAK_GLASS_PWD=$BREAK_GLASS_PWD
EOF
 
MSSQL_SA_PASSWORD="$SA_PWD" /opt/mssql/bin/mssql-conf -n set-sa-password
ls -l /root/.stoxx_sql_login.env
systemctl is-active mssql-server

Returns the password reset result, the credential-file permissions, and the stopped SQL Server state before restart.

ForceFlush is enabled for this instance.
Failed to open password policy registry path. Using default password policy values.
BulkAdmin AllowedPathsList cleared (path filtering disabled)
ForceFlush feature is enabled for log durability.
Configuring SQL Server...
The system administrator password has been changed.
Please run 'sudo systemctl start mssql-server' to start SQL Server.
-rw------- 1 root root 113 Apr 13 12:54 /root/.stoxx_sql_login.env
inactive

Restart SQL Server and validate the service

Starts SQL Server again on the VM and inspects the service state after the password reset.

sudo systemctl start mssql-server
sleep 8
sudo systemctl status mssql-server --no-pager | head -12

Confirms that the SQL Server Linux service returned to an active running state.

● mssql-server.service - Microsoft SQL Server Database Engine
     Loaded: loaded (/lib/systemd/system/mssql-server.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2026-04-13 12:55:10 UTC; 13s ago
       Docs: https://docs.microsoft.com/en-us/sql/linux
   Main PID: 19605 (sqlservr)
      Tasks: 151
     Memory: 642.0M
        CPU: 13.693s
     CGroup: /system.slice/mssql-server.service
             ├─19605 /opt/mssql/bin/sqlservr
             └─19608 /opt/mssql/bin/sqlservr

Create the named break-glass sysadmin login

Creates the dba_break_glass SQL login if it does not exist, grants it sysadmin, and prints the final login state.

source /root/.stoxx_sql_login.env
 
cat > /tmp/create_break_glass.sql <<SQL
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = N'dba_break_glass')
BEGIN
    CREATE LOGIN [dba_break_glass]
        WITH PASSWORD = N'$BREAK_GLASS_PWD',
             CHECK_POLICY = ON,
             CHECK_EXPIRATION = OFF,
             DEFAULT_DATABASE = [master];
END;
IF IS_SRVROLEMEMBER(N'sysadmin', N'dba_break_glass') <> 1
BEGIN
    ALTER SERVER ROLE [sysadmin] ADD MEMBER [dba_break_glass];
END;
SELECT name, type_desc, is_disabled, IS_SRVROLEMEMBER(N'sysadmin', name) AS is_sysadmin
FROM sys.server_principals
WHERE name IN (N'sa', N'dba_break_glass')
ORDER BY name;
SQL
 
/opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U sa -P "$SA_PWD" -C -b -W -s "|" -h -1 \
  -i /tmp/create_break_glass.sql

Shows that both sa and dba_break_glass are enabled SQL logins with sysadmin membership.

dba_break_glass|SQL_LOGIN|0|1
sa|SQL_LOGIN|0|1

Create the pipeline login and grant transform permissions

The deployed bronze loader and transform jobs do not run as sa. They use the dedicated SQL login stoxx_pipeline.

The two provisioning scripts used here are provision_pipeline_login.sql and grant_pipeline_transform_permissions.sql. Both now live in 02-stoxx-index-pipeline-source-files.

Run them as the named sysadmin:

Executes the pipeline-login creation script in master and the schema-permission grant script in stoxx.

source /root/.stoxx_sql_login.env
 
/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U "$BREAK_GLASS_LOGIN" -P "$BREAK_GLASS_PWD" \
  -d master -v PipelinePwd='REDACTED-RUNTIME-ONLY' \
  -i /opt/stoxx/ddl/provision_pipeline_login.sql
 
/opt/mssql-tools18/bin/sqlcmd -C -S localhost -U "$BREAK_GLASS_LOGIN" -P "$BREAK_GLASS_PWD" \
  -d stoxx \
  -i /opt/stoxx/ddl/grant_pipeline_transform_permissions.sql

Validate the final database roles from the live VM:

Queries SQL Server role membership to verify that stoxx_pipeline has the bronze loader and transformer roles.

SELECT r.name AS role_name, m.name AS member_name
FROM stoxx.sys.database_role_members drm
INNER JOIN stoxx.sys.database_principals r
    ON r.principal_id = drm.role_principal_id
INNER JOIN stoxx.sys.database_principals m
    ON m.principal_id = drm.member_principal_id
WHERE r.name IN ('bronze_loader','pipeline_transformer')
ORDER BY r.name, m.name;
role_namemember_name
bronze_loaderstoxx_pipeline
pipeline_transformerstoxx_pipeline

Validate the final split-file layout:

Queries sys.master_files to prove the database still uses the split data and log layout required on the VM.

SELECT name, type_desc, physical_name, size * 8 / 1024 AS size_mb
FROM sys.master_files
WHERE database_id = DB_ID('stoxx')
ORDER BY file_id;
nametype_descphysical_namesize_mb
stoxx_PrimaryROWS/mnt/sqldata/stoxx_Primary.mdf256
stoxx_LogLOG/mnt/sqllog/stoxx_Log.ldf256
stoxx_Current_01ROWS/mnt/sqldata/stoxx_Current_01.ndf512
stoxx_Current_02ROWS/mnt/sqldata/stoxx_Current_02.ndf512
stoxx_Archive_01ROWS/mnt/sqldata/stoxx_Archive_01.ndf256

Validate the active medallion scope:

Counts the SQL tables present in the bronze, silver, and gold schemas.

SELECT s.name AS schema_name, COUNT(*) AS table_count
FROM sys.tables t
INNER JOIN sys.schemas s
    ON s.schema_id = t.schema_id
WHERE s.name IN ('bronze','silver','gold')
GROUP BY s.name
ORDER BY s.name;
schema_nametable_count
bronze11
gold3
silver6

Lists the active index definitions that drive the end-to-end pipeline scope.

SELECT index_key, display_name, file_prefix
FROM bronze.dim_index
ORDER BY index_key;
index_keydisplay_namefile_prefix
euro_stoxx_50Euro Stoxx 50eurostoxx50
stoxx_asia_50STOXX Asia/Pacific 50stoxxasia50
stoxx_usa_50STOXX USA 50stoxxusa50

Create the stage bucket

The stage bucket is the raw landing zone. It is private, region-local, and predictable. The fetch job writes JSON there, and the bronze loader consumes those exact objects.

Create and inspect the bucket

Creates the regional GCS stage bucket and prints its storage configuration for verification.

gcloud storage buckets create gs://stoxx-stage-bucket \
  --project=bq-wh-nb \
  --location=europe-west1 \
  --uniform-bucket-level-access \
  --public-access-prevention
 
gcloud storage buckets describe gs://stoxx-stage-bucket

Confirms the bucket name, region, and access controls used for raw-stage storage.

creation_time: 2026-04-13T13:39:24+0000
location: EUROPE-WEST1
name: stoxx-stage-bucket
public_access_prevention: enforced
storage_url: gs://stoxx-stage-bucket/
uniform_bucket_level_access: true

Create the Airflow VM and install Airflow 3.2

The VM stoxx-airflow is a private Compute Engine host that runs Airflow 3.2.0 in Docker Compose. It owns orchestration only. It does not perform direct data transformation itself; it triggers Cloud Run jobs that do the operational work.

Create the VM

Creates the private Compute Engine VM that will host the Airflow control plane.

gcloud compute instances create stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --machine-type=e2-standard-2 \
  --image-family=ubuntu-2204-lts \
  --image-project=ubuntu-os-cloud \
  --boot-disk-size=30GB \
  --boot-disk-type=pd-balanced \
  --no-address \
  --service-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --scopes=https://www.googleapis.com/auth/cloud-platform \
  --metadata="enable-oslogin=TRUE" \
  --tags="airflow,iap-ssh" \
  --labels="app=stoxx-airflow,env=dev"

Connects to the new VM over IAP and verifies the hostname and Ubuntu image version.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="hostname && lsb_release -ds"

Shows that the new orchestration VM is reachable and running the expected Ubuntu release.

stoxx-airflow
Ubuntu 22.04.5 LTS

Install Docker Engine and Compose

Installs Docker Engine, Compose, and the required package repositories on the Airflow VM.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="
    sudo apt-get update &&
    sudo apt-get install -y ca-certificates curl gnupg lsb-release &&
    sudo install -m 0755 -d /etc/apt/keyrings &&
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg |
      sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg &&
    sudo chmod a+r /etc/apt/keyrings/docker.gpg &&
    echo 'deb [arch='\"'\"'$(dpkg --print-architecture)'\"'\"' signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu '\"'\"'$(. /etc/os-release && echo $VERSION_CODENAME)'\"'\"' stable' |
      sudo tee /etc/apt/sources.list.d/docker.list > /dev/null &&
    sudo apt-get update &&
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin &&
    sudo systemctl enable --now docker &&
    sudo usermod -aG docker \$USER
  "

Prints the user identity, Docker Engine version, and Docker Compose version after installation.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="id && docker --version && docker compose version"

Confirms that the operator account joined the docker group and that the expected Docker tooling is available.

uid=1137701540(alexper_recovery_gmail_com) ... groups=...,999(docker)
Docker version 29.4.0, build 9d7ad9f
Docker Compose version v5.1.2

Add the Airflow Google connection and Cloud Run execution rights

The Airflow DAG uses CloudRunExecuteJobOperator. Two setup items are mandatory on a fresh install:

  1. create google_cloud_default
  2. grant roles/run.developer to the VM service account

Creates the google_cloud_default Airflow connection inside the running worker container.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="
    cd /home/alexper_recovery_gmail_com/app &&
    docker compose exec -T airflow-worker \
      airflow connections add google_cloud_default \
      --conn-uri 'google-cloud-platform://'
  "

Returns the successful creation of the default Google Cloud connection required by the DAG operators.

Successfully added `conn_id`=google_cloud_default : google-cloud-platform://

Grants Cloud Run execution rights to the VM service account used by Airflow.

gcloud projects add-iam-policy-binding bq-wh-nb \
  --member=\"serviceAccount:bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com\" \
  --role=\"roles/run.developer\" \
  --condition=None

Confirms that the IAM policy update completed successfully.

Updated IAM policy for project [bq-wh-nb].

Validate the live connection:

Reads the Airflow connection back from the running worker to verify the final connection object.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="
    cd /home/alexper_recovery_gmail_com/app &&
    sudo docker compose exec -T airflow-worker airflow connections get google_cloud_default
  "

Prints the stored Airflow connection metadata for google_cloud_default.

id | conn_id              | conn_type             | description | host | schema | login | password | port | is_encrypted | is_extra_encrypted | extra_dejson | get_uri
===+======================+=======================+=============+======+========+=======+==========+======+==============+====================+==============+=========================
1  | google_cloud_default | google_cloud_platform | None        |      |        | None  | None     | None | False        | False              | {}           | google-cloud-platform://

Validate the final Airflow runtime

Lists the Docker Compose services that make up the Airflow deployment on the VM.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="cd /home/alexper_recovery_gmail_com/app && sudo docker compose ps"

Returns the final healthy state of the Airflow API server, scheduler, dag processor, triggerer, worker, Redis, and Postgres containers.

NAME                          IMAGE                 COMMAND                  SERVICE                 CREATED             STATUS                       PORTS
app-airflow-apiserver-1       stoxx-airflow:3.2.0   "/usr/bin/dumb-init …"   airflow-apiserver       About an hour ago   Up About an hour (healthy)   0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp
app-airflow-dag-processor-1   stoxx-airflow:3.2.0   "/usr/bin/dumb-init …"   airflow-dag-processor   About an hour ago   Up About an hour (healthy)   8080/tcp
app-airflow-scheduler-1       stoxx-airflow:3.2.0   "/usr/bin/dumb-init …"   airflow-scheduler       About an hour ago   Up About an hour (healthy)   8080/tcp
app-airflow-triggerer-1       stoxx-airflow:3.2.0   "/usr/bin/dumb-init …"   airflow-triggerer       About an hour ago   Up About an hour (healthy)   8080/tcp
app-airflow-worker-1          stoxx-airflow:3.2.0   "/usr/bin/dumb-init …"   airflow-worker          About an hour ago   Up About an hour (healthy)   8080/tcp
app-postgres-1                postgres:16           "docker-entrypoint.s…"   postgres                5 hours ago         Up 5 hours (healthy)         5432/tcp
app-redis-1                   redis:7.2-bookworm    "docker-entrypoint.s…"   redis                   5 hours ago         Up 5 hours (healthy)         6379/tcp

Airflow restarts have two noisy failure signals

Fresh restarts can produce misleading symptoms even when the stack is recoverable:

  • Health checks can report health: starting briefly during docker compose up -d or container recreation.
  • The DAG depends on SERVING_JOB; when the variable was unset, Compose logged The "SERVING_JOB" variable is not set. Defaulting to a blank string.

Treat both as validation issues, not as proof that the runtime is irrecoverable.

Validate the recovered Airflow runtime

Wait for the health checks to settle, confirm the final container state with docker compose ps and airflow jobs check, and keep SERVING_JOB: ${SERVING_JOB:-stoxx-serving} in docker-compose.yaml so restarted environments still resolve the serving job name.

Build and deploy the Cloud Run jobs

Four Cloud Run jobs implement the runtime boundary between orchestration and execution:

  1. stoxx-stage-fetch
  2. stoxx-bronze-load
  3. stoxx-transforms
  4. stoxx-serving

The split keeps each job aligned to one operational responsibility, one image, and one failure surface.

Build and deploy stoxx-stage-fetch

Builds the stoxx-stage-fetch container image and deploys the Cloud Run job that fetches yfinance data into GCS.

gcloud builds submit "C:\Users\aperi\My Drive\VAULT\.codex-temp\stoxx-stage-fetch" \
  --project=bq-wh-nb \
  --tag=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-stage-fetch:20260413-2
 
gcloud run jobs deploy stoxx-stage-fetch \
  --project=bq-wh-nb \
  --region=europe-west1 \
  --image=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-stage-fetch:20260413-2 \
  --service-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --cpu=2 \
  --memory=2Gi \
  --task-timeout=30m \
  --max-retries=0 \
  --set-env-vars=GCP_PROJECT_ID=bq-wh-nb,STAGE_BUCKET=stoxx-stage-bucket,OHLCV_LOOKBACK_DAYS=10,INFO_WORKERS=6,HISTORY_WORKERS=4,YF_RETRIES=5,JOB_NAME=stoxx-stage-fetch

Build and deploy stoxx-bronze-load

Builds the bronze loader image and deploys the Cloud Run job that reads the stage manifest and writes SQL bronze tables.

gcloud builds submit "C:\Users\aperi\My Drive\VAULT\.codex-temp\stoxx-bronze-load" \
  --project=bq-wh-nb \
  --tag=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-bronze-load:20260413-1
 
gcloud run jobs deploy stoxx-bronze-load \
  --project=bq-wh-nb \
  --region=europe-west1 \
  --image=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-bronze-load:20260413-1 \
  --service-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --cpu=2 \
  --memory=2Gi \
  --task-timeout=30m \
  --max-retries=0 \
  --network=default \
  --subnet=default \
  --vpc-egress=private-ranges-only \
  --set-env-vars=GCP_PROJECT_ID=bq-wh-nb,JOB_NAME=stoxx-bronze-load,MANIFEST_URI=gs://stoxx-stage-bucket/manifests/stoxx-stage-fetch/latest.json,SQL_HOST=10.132.0.8,SQL_PORT=1433,SQL_DATABASE=stoxx,SQL_USER=stoxx_pipeline \
  --set-secrets=SQL_PASSWORD=stoxx-pipeline-sql-password:latest

Build and deploy stoxx-transforms

Builds the SQL transform image and deploys the Cloud Run job that materializes silver and gold tables inside SQL Server.

gcloud builds submit "C:\Users\aperi\My Drive\VAULT\.codex-temp\stoxx-transforms" \
  --project=bq-wh-nb \
  --tag=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-transforms:20260413-1
 
gcloud run jobs deploy stoxx-transforms \
  --project=bq-wh-nb \
  --region=europe-west1 \
  --image=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-transforms:20260413-1 \
  --service-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --cpu=2 \
  --memory=4Gi \
  --task-timeout=30m \
  --max-retries=0 \
  --network=default \
  --subnet=default \
  --vpc-egress=private-ranges-only \
  --set-env-vars=DD_TRACE_ENABLED=false,SQL_HOST=10.132.0.8,SQL_PORT=1433,SQL_DATABASE=stoxx,SQL_USER=stoxx_pipeline \
  --set-secrets=SA_PASSWORD=stoxx-pipeline-sql-password:latest

Build and deploy stoxx-serving

Builds the serving image and deploys the Cloud Run job that syncs BigQuery replicas, builds marts, and publishes Firestore documents.

gcloud builds submit "C:\Users\aperi\My Drive\VAULT\.codex-temp\stoxx-serving" \
  --project=bq-wh-nb \
  --tag=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-serving:20260413-8
 
gcloud run jobs deploy stoxx-serving \
  --project=bq-wh-nb \
  --region=europe-west1 \
  --image=europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-serving:20260413-8 \
  --service-account=bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com \
  --cpu=2 \
  --memory=4Gi \
  --task-timeout=30m \
  --max-retries=0 \
  --network=default \
  --subnet=default \
  --vpc-egress=private-ranges-only \
  --set-env-vars=GCP_PROJECT_ID=bq-wh-nb,BQ_LOCATION=europe-west1,BQ_SILVER_DATASET=stoxx_silver,BQ_GOLD_DATASET=stoxx_gold,BQ_MARTS_DATASET=stoxx_marts,FIRESTORE_DATABASE=main,FIRESTORE_COLLECTION=stoxx_indices,LOG_LEVEL=INFO,SQL_HOST=10.132.0.8,SQL_PORT=1433,SQL_DATABASE=stoxx,SQL_USER=stoxx_pipeline \
  --set-secrets=SQL_PASSWORD=stoxx-pipeline-sql-password:latest

Live validation of the final serving job:

Describes the deployed stoxx-serving Cloud Run job to verify image, networking, secrets, and runtime settings.

gcloud run jobs describe stoxx-serving --project=bq-wh-nb --region=europe-west1

Returns the final deployed stoxx-serving job configuration that was validated during the rollout.

+ Job stoxx-serving in region europe-west1
Executed 16 times
Last executed 2026-04-13T18:14:04.297439Z with execution stoxx-serving-82g8s
Last updated on 2026-04-13T18:12:28.983750Z by alexper.recovery@gmail.com
 
Tasks:               1
Parallelism:         No limit
Container None
  Image:             europe-west1-docker.pkg.dev/bq-wh-nb/stoxx-demo/stoxx-serving:20260413-8
  Memory:            4Gi
  CPU:               2
  Env vars:
    BQ_GOLD_DATASET  stoxx_gold
    BQ_LOCATION      europe-west1
    BQ_MARTS_DATASET stoxx_marts
    BQ_SILVER_DATASET stoxx_silver
    FIRESTORE_COLLECTION stoxx_indices
    FIRESTORE_DATABASE main
    GCP_PROJECT_ID   bq-wh-nb
    LOG_LEVEL        INFO
    SQL_DATABASE     stoxx
    SQL_HOST         10.132.0.8
    SQL_PORT         1433
    SQL_USER         stoxx_pipeline
  Secrets:
    SQL_PASSWORD     stoxx-pipeline-sql-password:latest
Task Timeout:        30m
Max Retries:         0
Service account:     bq-wh-sa@bq-wh-nb.iam.gserviceaccount.com
VPC access:
  Network:           default
  Subnet:            default
  Egress:            private-ranges-only

Cloud Run deployment can fail at both network and SQL boundaries

Two configuration errors broke the first deployment path:

  • One serving deployment referenced a nonexistent VPC connector and failed with VPC connector projects/bq-wh-nb/locations/europe-west1/connectors/default does not exist, or Cloud Run does not have permission to use it.
  • The first OHLCV transform failed with The SELECT permission was denied on the object 'eurostoxx50_ohlcv', database 'stoxx', schema 'silver'. (229) because the pipeline login did not yet hold the required transform role membership.

Deploy against the real runtime path

Use direct --network=default --subnet=default --vpc-egress=private-ranges-only for jobs that must reach the SQL VM privately, then run grant_pipeline_transform_permissions.sql and verify that stoxx_pipeline is a member of pipeline_transformer.

Provision BigQuery, Firestore, and Eventarc

The serving half of the pipeline executes five ordered steps:

  1. copy SQL outputs into BigQuery
  2. build BigQuery marts
  3. publish serving projections to Firestore
  4. update serving_control/current
  5. let Eventarc catch that control-document write

Firestore database

Describes the Firestore database used for the serving layer and the control document.

gcloud firestore databases describe \
  --project=bq-wh-nb \
  --database=main

Confirms that the serving database is main, uses native mode, and has realtime updates enabled.

appEngineIntegrationMode: DISABLED
concurrencyMode: PESSIMISTIC
createTime: '2026-04-05T07:38:52.956876Z'
databaseEdition: STANDARD
locationId: europe-west1
name: projects/bq-wh-nb/databases/main
realtimeUpdatesMode: REALTIME_UPDATES_MODE_ENABLED
type: FIRESTORE_NATIVE

Deploy the Eventarc receiver service

Deploys the Cloud Run service that receives Firestore CloudEvents from Eventarc.

gcloud run deploy stoxx-firestore-eventarc-receiver \
  --project=bq-wh-nb \
  --region=europe-west1 \
  --source="C:\Users\aperi\My Drive\VAULT\.codex-temp\firestore-control-eventarc-receiver" \
  --allow-unauthenticated

Describes the deployed receiver service to verify its URL, revision, scaling, and service account.

gcloud run services describe stoxx-firestore-eventarc-receiver \
  --project=bq-wh-nb \
  --region=europe-west1

Returns the active Eventarc receiver revision and the exposed service endpoint.

+ Service stoxx-firestore-eventarc-receiver in region europe-west1
 
URL:     https://stoxx-firestore-eventarc-receiver-348557092514.europe-west1.run.app
Ingress: all
Traffic:
  100% LATEST (currently stoxx-firestore-eventarc-receiver-00001-j8b)
 
Scaling: Auto (Min: 0, Max: 20)
 
Last updated on 2026-04-13T18:06:40.134448Z by alexper.recovery@gmail.com:
  Revision stoxx-firestore-eventarc-receiver-00001-j8b
  Service account:   348557092514-compute@developer.gserviceaccount.com
  Concurrency:       80
  Timeout:           300s

Create the Eventarc trigger on the Firestore control document

Creates the Eventarc service identity and then provisions the trigger that watches serving_control/current.

gcloud beta services identity create \
  --service=eventarc.googleapis.com \
  --project=bq-wh-nb
 
gcloud eventarc triggers create stoxx-firestore-control-written \
  --project=bq-wh-nb \
  --location=europe-west1 \
  --destination-run-service=stoxx-firestore-eventarc-receiver \
  --destination-run-region=europe-west1 \
  --event-filters="type=google.cloud.firestore.document.v1.written" \
  --event-filters="database=main" \
  --event-filters-path-pattern="document=serving_control/current" \
  --service-account=stoxx-eventarc-trigger@bq-wh-nb.iam.gserviceaccount.com

First failure before the Eventarc service identity existed:

Captures the initial permission failure that occurred before the Eventarc service agent was ready.

ERROR: (gcloud.eventarc.triggers.create) FAILED_PRECONDITION: Invalid resource state for "": Permission denied while using the Eventarc Service Agent. If you recently started to use Eventarc, it may take a few minutes before all necessary permissions are propagated to the Service Agent.

Successful retry:

Returns the successful trigger creation after the Eventarc service identity had propagated.

Creating trigger [stoxx-firestore-control-written] in project [bq-wh-nb], location [europe-west1]...
..............................................................................................done.
WARNING: It may take up to 2 minutes for the new trigger to become active.

Validate the live trigger:

Describes the Eventarc trigger to verify the final Firestore filters and Pub/Sub transport resources.

gcloud eventarc triggers describe stoxx-firestore-control-written \
  --project=bq-wh-nb \
  --location=europe-west1

Prints the trigger filters, destination service, and managed transport topic and subscription.

createTime: '2026-04-13T18:08:05.523266022Z'
destination:
  cloudRun:
    region: europe-west1
    service: stoxx-firestore-eventarc-receiver
eventDataContentType: application/protobuf
eventFilters:
- attribute: type
  value: google.cloud.firestore.document.v1.written
- attribute: database
  value: main
- attribute: document
  operator: match-path-pattern
  value: serving_control/current
serviceAccount: stoxx-eventarc-trigger@bq-wh-nb.iam.gserviceaccount.com
transport:
  pubsub:
    subscription: projects/bq-wh-nb/subscriptions/eventarc-europe-west1-stoxx-firestore-control-written-sub-850
    topic: projects/bq-wh-nb/topics/eventarc-europe-west1-stoxx-firestore-control-written-166

Firestore database identity is explicit

The serving layer does not target the default Firestore database. It targets main, so publishing or trigger creation against (default) would miss the real serving writes.

Bind both publisher and trigger to main

Set FIRESTORE_DATABASE=main in stoxx-serving, and create the Eventarc trigger with database=main.

Deploy the Airflow DAG

Airflow orchestrates the pipeline through one manual DAG named stoxx_stage_yfinance. It fans out only where parallelism is safe: the three silver transforms run in parallel after the bronze load, but the pipeline remains strictly ordered across the major business boundaries.

The DAG source file stoxx_stage_yfinance.py now lives in 02-stoxx-index-pipeline-source-files.

Copy the DAG and restart Airflow

Copy the DAG and restart the Airflow services:

Copies the final DAG file onto the Airflow VM and restarts the Compose stack so the scheduler and worker load it.

gcloud compute scp \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  "C:\Users\aperi\My Drive\VAULT\.codex-temp\airflow-vm\dags\stoxx_stage_yfinance.py" \
  stoxx-airflow:/home/alexper_recovery_gmail_com/app/dags/stoxx_stage_yfinance.py
 
gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="
    cd /home/alexper_recovery_gmail_com/app &&
    sudo docker compose up -d
  "

Validate the full DAG run

The final fully successful end-to-end run used for this note was:

  • DAG run id: manual__2026-04-13T17:28:30Z_serving
  • start: 2026-04-13 17:30:26 UTC
  • finish: 2026-04-13 17:41:44 UTC

Inspect the final task states

Queries Airflow for the task-level states of the final successful end-to-end DAG run.

gcloud compute ssh stoxx-airflow \
  --project=bq-wh-nb \
  --zone=europe-west1-b \
  --tunnel-through-iap \
  --command="
    cd /home/alexper_recovery_gmail_com/app &&
    sudo docker compose exec -T airflow-worker \
      airflow tasks states-for-dag-run stoxx_stage_yfinance manual__2026-04-13T17:28:30Z_serving
  "

Shows that every task in the final end-to-end run completed in the success state.

dag_id               | logical_date | task_id                               | state   | start_date                       | end_date
=====================+==============+=======================================+=========+==================================+=================================
stoxx_stage_yfinance |              | build_gold_scores                     | success | 2026-04-13T17:34:07.755655+00:00 | 2026-04-13T17:35:19.502117+00:00
stoxx_stage_yfinance |              | build_bigquery_marts                  | success | 2026-04-13T17:38:08.176627+00:00 | 2026-04-13T17:39:31.636842+00:00
stoxx_stage_yfinance |              | transform_ohlcv_to_silver             | success | 2026-04-13T17:32:52.958397+00:00 | 2026-04-13T17:34:07.065424+00:00
stoxx_stage_yfinance |              | build_gold_index_performance          | success | 2026-04-13T17:35:20.521940+00:00 | 2026-04-13T17:36:29.693701+00:00
stoxx_stage_yfinance |              | load_bronze_into_sql                  | success | 2026-04-13T17:31:54.675515+00:00 | 2026-04-13T17:32:52.180176+00:00
stoxx_stage_yfinance |              | sync_gold_to_bigquery                 | success | 2026-04-13T17:36:30.357505+00:00 | 2026-04-13T17:38:07.571071+00:00
stoxx_stage_yfinance |              | publish_serving_to_firestore          | success | 2026-04-13T17:39:32.834998+00:00 | 2026-04-13T17:40:40.082303+00:00
stoxx_stage_yfinance |              | fetch_bronze_stage_into_gcs           | success | 2026-04-13T17:30:27.650234+00:00 | 2026-04-13T17:31:54.180281+00:00
stoxx_stage_yfinance |              | transform_signals_daily_to_silver     | success | 2026-04-13T17:32:53.576918+00:00 | 2026-04-13T17:33:59.913948+00:00
stoxx_stage_yfinance |              | transform_signals_quarterly_to_silver | success | 2026-04-13T17:32:53.740656+00:00 | 2026-04-13T17:34:04.283928+00:00
stoxx_stage_yfinance |              | validate_serving_layer                | success | 2026-04-13T17:40:40.571297+00:00 | 2026-04-13T17:41:44.811851+00:00

Replay reset procedure

The replay workflow requires a short reset so the most recent processing window can be reloaded and the downstream inserts remain observable.

The reset scripts reset_recent_stoxx_demo_window.sql and reset_recent_stoxx_demo_window.ps1 now live in 02-stoxx-index-pipeline-source-files.

Execute the reset before a replay run

Execute the reset with:

Uploads the reset SQL to the SQL VM and trims the recent bronze, silver, and gold windows so the next run produces observable inserts again.

& 'C:\Users\aperi\My Drive\VAULT\.codex-temp\stoxx-transforms\reset_recent_stoxx_demo_window.ps1' -DaysBack 3

Pipeline execution path

The execution path below traces one successful publish from raw landing to downstream event emission. Each stage shows the inspection command or query used during validation and a bounded result that proves the state transition at that boundary.

External acquisition and GCS landing

stoxx-stage-fetch acquires current market and fundamentals data from yfinance and lands one JSON object per logical entity in the stage bucket. The job reads the three index definition files, fetches .info plus recent OHLCV history, writes dimensions/, stage/, and pulse/ objects, and finalizes the landing set by updating manifests/stoxx-stage-fetch/latest.json.

Inspect the manifest

Reads the latest stage manifest from GCS to verify the publish timestamp, the number of covered indices, the number of symbols, and the absence of fetch errors.

gcloud storage cat gs://stoxx-stage-bucket/manifests/stoxx-stage-fetch/latest.json

Returns the landing manifest written by the fetch job.

{
  "generated_at": "2026-04-13T17:31:42Z",
  "index_count": 3,
  "symbol_count": 150,
  "errors": []
}

Inspect the object inventory

Lists the staged JSON objects in GCS so the reader can see the raw landing files produced for the bronze layer.

gcloud storage ls -l "gs://stoxx-stage-bucket/stage/*.json"

Returns the staged OHLCV and signal payload files stored in the raw landing bucket.

675705  2026-04-13T17:31:41Z  gs://stoxx-stage-bucket/stage/eurostoxx50_ohlcv.json
239270  2026-04-13T17:31:41Z  gs://stoxx-stage-bucket/stage/eurostoxx50_signals_daily.json
222329  2026-04-13T17:31:41Z  gs://stoxx-stage-bucket/stage/eurostoxx50_signals_quarterly.json
...
TOTAL: 9 objects, 675705 bytes (659.87 KiB)

Inspect a staged OHLCV payload

Reads the staged Euro STOXX 50 OHLCV JSON payload that the bronze loader will convert into SQL rows.

gcloud storage cat gs://stoxx-stage-bucket/stage/eurostoxx50_ohlcv.json

Sample from the staged eurostoxx50_ohlcv.json payload:

symboldateopenhighlowclosevolume
ASML.AS2026-03-271157.01163.01129.81146.4641843
ASML.AS2026-03-301136.41155.41096.21112.0735589
ASML.AS2026-03-311101.01126.81080.21119.2874917
ASML.AS2026-04-011159.01190.01143.41187.6741501
ASML.AS2026-04-021137.01174.01126.61161.0647486

Bronze ingestion into SQL Server

stoxx-bronze-load consumes the stage manifest, downloads each landed object, maps it to its target bronze table, and applies the appropriate load semantics by entity grain. OHLCV is merged by (symbol, date), while daily and quarterly signal entities are reloaded as the current operational snapshot.

Inspect the bronze OHLCV landing table

Queries the current bronze OHLCV landing table for the latest rows written by the loader.

SELECT TOP 5 symbol, [date], [open], high, low, [close], volume
FROM bronze.eurostoxx50_ohlcv
ORDER BY [date] DESC, symbol;
symboldateopenhighlowclosevolume
ABI.BR2026-04-1364.1464.2863.663.861870889
AD.AS2026-04-1341.1241.5241.1141.271411723
ADS.DE2026-04-13136.25136.75135.25136.0459029
ADYEN.AS2026-04-13852.0893.0843.0892.4147920
AI.PA2026-04-13187.5188.38186.24187.5722197

The bronze OHLCV tables retain only the current operational window after silver has absorbed the new data, so bronze remains a landing layer rather than a historical store.

Inspect bronze retention behavior

Counts the bronze OHLCV rows and checks the minimum and maximum dates to prove that bronze keeps only the latest operational window.

SELECT COUNT(*) AS bronze_ohlcv_rows, MIN([date]) AS min_date, MAX([date]) AS max_date
FROM bronze.eurostoxx50_ohlcv;
bronze_ohlcv_rowsmin_datemax_date
502026-04-132026-04-13

Inspect the bronze daily signal snapshot

Queries the current bronze daily signal table to show the latest per-symbol daily metrics loaded from the staged JSON.

SELECT TOP 5 _index, symbol, CAST([timestamp] AS date) AS signal_date, forward_pe, price_to_book, upside_potential
FROM bronze.signals_daily
ORDER BY signal_date DESC, _index, symbol;
_indexsymbolsignal_dateforward_peprice_to_bookupside_potential
euro_stoxx_50ABI.BR2026-04-1315.6249161.65413750.17018728468524902
euro_stoxx_50AD.AS2026-04-1313.8879082.5784080.0055730554882480465
euro_stoxx_50ADS.DE2026-04-1311.6247564.20674940.39705882352941169
euro_stoxx_50ADYEN.AS2026-04-1318.6838875.324710.59681757059614537
euro_stoxx_50AI.PA2026-04-1323.8699344.13387160.06666666666666665

Silver transformation

The silver layer converts bronze snapshots into historized, queryable facts at business grain.

  • OHLCV is gap-filled by exchange trading calendar and historized in silver.*_ohlcv.
  • daily signals are upserted by (_index, symbol, signal_date).
  • quarterly signals are upserted by (_index, symbol, as_of_date).

Inspect silver OHLCV history

Queries the silver OHLCV history table to show the cleaned and historized rows produced from the bronze landing layer.

SELECT TOP 5 symbol, [date], [open], high, low, [close], is_filled
FROM silver.eurostoxx50_ohlcv
ORDER BY [date] DESC, symbol;
symboldateopenhighlowcloseis_filled
ABI.BR2026-04-1364.1464.2863.663.860
AD.AS2026-04-1341.1241.5241.1141.270
ADS.DE2026-04-13136.25136.75135.25136.00
ADYEN.AS2026-04-13852.0893.0843.0892.40
AI.PA2026-04-13187.5188.38186.24187.50

Silver OHLCV retains the analytical history used by downstream scoring and index calculations.

Inspect silver history depth

Counts the historized silver OHLCV rows and checks the time span retained for analytical use.

SELECT COUNT(*) AS silver_ohlcv_rows, MIN([date]) AS min_date, MAX([date]) AS max_date
FROM silver.eurostoxx50_ohlcv;
silver_ohlcv_rowsmin_datemax_date
673552021-01-042026-04-13

Inspect the current silver dimensions

Queries the current silver dimension rows that enrich downstream scoring and serving steps with sector, country, and exchange metadata.

SELECT TOP 5 _index, symbol, sector, country, exchange, is_current
FROM silver.index_dim
WHERE is_current = 1
ORDER BY _index, symbol;
_indexsymbolsectorcountryexchangeis_current
euro_stoxx_50ABI.BRConsumer DefensiveBelgiumBRU1
euro_stoxx_50AD.ASConsumer DefensiveNetherlandsAMS1
euro_stoxx_50ADS.DEConsumer CyclicalGermanyGER1
euro_stoxx_50ADYEN.ASTechnologyNetherlandsAMS1
euro_stoxx_50AI.PABasic MaterialsFrancePAR1

Inspect historized silver daily signals

Queries silver daily signals to show how the bronze snapshot was upserted into a historical daily fact table.

SELECT TOP 5 _index, symbol, signal_date, forward_pe, price_to_book, upside_potential
FROM silver.signals_daily
ORDER BY signal_date DESC, _index, symbol;
_indexsymbolsignal_dateforward_peprice_to_bookupside_potential
euro_stoxx_50ABI.BR2026-04-1315.6249161.65413750.17018728468524902
euro_stoxx_50AD.AS2026-04-1313.8879082.5784080.0055730554882480465
euro_stoxx_50ADS.DE2026-04-1311.6247564.20674940.39705882352941169
euro_stoxx_50ADYEN.AS2026-04-1318.6838875.324710.59681757059614537
euro_stoxx_50AI.PA2026-04-1323.8699344.13387160.06666666666666665

Inspect historized silver quarterly signals

Queries silver quarterly signals to show the persisted fiscal-quarter snapshot used later by the gold quality and governance models.

SELECT TOP 5 _index, symbol, as_of_date, gross_margins, return_on_equity, overall_risk
FROM silver.signals_quarterly
ORDER BY as_of_date DESC, _index, symbol;
_indexsymbolas_of_dategross_marginsreturn_on_equityoverall_risk
stoxx_asia_503382.T2026-02-280.162020.076051
stoxx_asia_509983.T2026-02-280.541690.206165
stoxx_usa_50ORCL2026-02-280.670840.575729
stoxx_usa_50MU2026-02-260.584370.398230023
stoxx_usa_50COST2026-02-150.129260.296509981

Gold transformation

The gold layer derives business-facing metrics and ranking outputs from the historized silver inputs.

  • gold.scores_daily calculates value, momentum, and sentiment signals per stock.
  • gold.scores_quarterly calculates quality, health, and governance metrics.
  • gold.index_performance calculates index-level time series such as daily return, rolling return, volatility, and aggregate valuation.

Inspect daily gold scores

Queries the daily gold score table to show ranked per-stock analytics derived from silver daily signals and dimensions.

SELECT TOP 5 _index, symbol, score_date, composite_score, composite_rank, market_cap, index_weight
FROM gold.scores_daily
ORDER BY score_date DESC, _index, composite_rank, symbol;
_indexsymbolscore_datecomposite_scorecomposite_rankmarket_capindex_weight
euro_stoxx_50BNP.PA2026-04-130.650270236415652621988906864640.019382468154344679
euro_stoxx_50VOW.DE2026-04-130.466854998189972112452168294400.0088624499232319836
euro_stoxx_50IFX.DE2026-04-130.436974593341798113559008604160.010956508499999355
euro_stoxx_50TTE.PA2026-04-130.4318206230639751741699716464640.033314259841136062
euro_stoxx_50ENI.MI2026-04-130.421997365375153425706171125760.013840878090722576

Inspect quarterly gold scores

Queries the quarterly gold score table to show quality and governance metrics built from silver quarterly fundamentals.

SELECT TOP 5 _index, symbol, as_of_date, quality_score, quality_rank, governance_score, governance_rank
FROM gold.scores_quarterly
ORDER BY as_of_date DESC, _index, quality_rank, symbol;
_indexsymbolas_of_datequality_scorequality_rankgovernance_scoregovernance_rank
stoxx_asia_509983.T2026-02-280.6375223208076927855.614
stoxx_asia_503382.T2026-02-28-1.1863896449499411509.01
stoxx_usa_50ORCL2026-02-28-0.89927951752687885493.019
stoxx_usa_50MU2026-02-260.37180987104078544126.27
stoxx_usa_50COST2026-02-15-0.51225504585330928427.04

Inspect gold index performance

Queries the gold index-performance table to show the index-level time series generated from silver OHLCV and daily signals.

SELECT TOP 5 _index, perf_date, daily_return, rolling_30d_return, ytd_return, stocks_count
FROM gold.index_performance
ORDER BY perf_date DESC, _index;
_indexperf_datedaily_returnrolling_30d_returnytd_returnstocks_count
euro_stoxx_502026-04-13-0.0038324561027185972-0.044198430631456675-0.00765087514968376750
stoxx_asia_502026-04-130.00085207511964490248-0.0122980002503848550.08898338798884886112
stoxx_usa_502026-04-130.00445219831699007140.000336237823699958580.04140848898999638450
euro_stoxx_502026-04-100.0020284551930983686-0.037822516443515086-0.003833109269979262450
stoxx_asia_502026-04-100.0014575875829731543-0.0261766590825539550.08805628230192619250

BigQuery analytical replica refresh

stoxx-serving --mode=sync-replica refreshes the cloud analytical copy of the selected SQL outputs so downstream reads and mart builds do not execute on the operational SQL VM. The replica step recreates and reloads:

  • stoxx_silver.index_dim
  • stoxx_gold.scores_daily
  • stoxx_gold.scores_quarterly
  • stoxx_gold.index_performance

The replica boundary separates operational SQL writes from analytical reads.

Inspect the replicated BigQuery gold table

Queries the replicated BigQuery daily gold table to verify that SQL gold was copied into the analytics layer without changing the business grain.

SELECT _index, symbol, score_date, composite_score, composite_rank
FROM `bq-wh-nb.stoxx_gold.scores_daily`
ORDER BY score_date DESC, _index, composite_rank, symbol
LIMIT 5;
_indexsymbolscore_datecomposite_scorecomposite_rank
euro_stoxx_50BNP.PA2026-04-130.65027023641565261
euro_stoxx_50VOW.DE2026-04-130.46685499818997212
euro_stoxx_50IFX.DE2026-04-130.43697459334179813
euro_stoxx_50TTE.PA2026-04-130.43182062306397524
euro_stoxx_50ENI.MI2026-04-130.42199736537515345

BigQuery datamart construction

stoxx-serving --mode=build-marts reshapes the replica layer into serving-oriented analytical models. The job builds four primary datamarts:

  • mart_constituent_screener_latest
  • mart_sector_heatmap_latest
  • mart_index_compare_history
  • mart_index_factsheet_latest

The datamart layer exposes ranking and performance fields that are not self-explanatory without a field reference.

Key fields in mart_constituent_screener_latest:

FieldMeaningOperational use
score_dateTrading date for the daily score snapshot.Distinguishes the serving snapshot from earlier rebuilds.
composite_scoreWeighted daily score built from value, momentum, and sentiment inputs. Higher is stronger.Primary sorting metric for the constituent screener.
composite_rankRank of composite_score within one index on one score_date; 1 is strongest.Determines display order and top-constituent selection.
sectorLatest serving-sector classification for the stock.Supports filtering and sector-level rollups.

Key fields in mart_index_compare_history:

FieldMeaningOperational use
base_100Rebased cumulative performance series where the comparison baseline is normalized near 100.Makes cross-index performance curves directly comparable.
daily_returnOne-day percentage return stored as a decimal fraction.Shows the latest move before compounding.
rolling_30d_returnThirty-trading-day cumulative return stored as a decimal fraction.Highlights short-term trend strength or drawdown.
ytd_returnYear-to-date return stored as a decimal fraction.Shows calendar-year performance through the current row.

Inspect the factsheet mart

Queries the index factsheet mart and extracts the first symbol from the nested top-constituents array to limit output width while preserving the document shape.

SELECT
  _index,
  as_of_date,
  avg_composite_score,
  stocks_count,
  top_constituents[OFFSET(0)].symbol AS top_symbol
FROM `bq-wh-nb.stoxx_marts.mart_index_factsheet_latest`
ORDER BY _index;

Sample from mart_index_factsheet_latest:

_indexas_of_dateavg_composite_scorestocks_counttop_symbol
euro_stoxx_502026-04-130.0192311940374905850BNP.PA
stoxx_asia_502026-04-130.023128419284850904124568.T
stoxx_usa_502026-04-130.00837640827847368350MU

Inspect the constituent screener mart

Queries the constituent screener mart for the highest-ranked rows so the per-stock serving grain is visible.

SELECT _index, symbol, score_date, composite_score, composite_rank, sector
FROM `bq-wh-nb.stoxx_marts.mart_constituent_screener_latest`
ORDER BY _index, composite_rank, symbol
LIMIT 5;

Sample from mart_constituent_screener_latest:

_indexsymbolscore_datecomposite_scorecomposite_ranksector
euro_stoxx_50BNP.PA2026-04-130.65027023641565261Financial Services
euro_stoxx_50VOW.DE2026-04-130.46685499818997212Consumer Cyclical
euro_stoxx_50IFX.DE2026-04-130.43697459334179813Technology
euro_stoxx_50TTE.PA2026-04-130.43182062306397524Energy
euro_stoxx_50ENI.MI2026-04-130.42199736537515345Energy

Inspect the sector heatmap mart

Queries the sector heatmap mart for the heaviest Euro STOXX 50 sectors so the aggregated serving grain is visible.

SELECT _index, score_date, sector, stock_count, total_weight, avg_composite_score
FROM `bq-wh-nb.stoxx_marts.mart_sector_heatmap_latest`
WHERE _index = 'euro_stoxx_50'
ORDER BY total_weight DESC, sector
LIMIT 5;

Sample from mart_sector_heatmap_latest:

_indexscore_datesectorstock_counttotal_weightavg_composite_score
euro_stoxx_502026-04-13Financial Services110.2021670.03288874631223965
euro_stoxx_502026-04-13Industrials100.191120.04528305170779484
euro_stoxx_502026-04-13Consumer Cyclical90.17629-0.10734261620293291
euro_stoxx_502026-04-13Technology50.1494380.08290680291651072
euro_stoxx_502026-04-13Consumer Defensive40.077242-0.1491554676074793

Inspect the historical compare mart

Queries the historical index-compare mart for the latest rows so the base-100 series and rolling performance metrics are visible.

SELECT _index, perf_date, base_100, daily_return, rolling_30d_return, ytd_return
FROM `bq-wh-nb.stoxx_marts.mart_index_compare_history`
ORDER BY perf_date DESC, _index
LIMIT 5;

Sample from mart_index_compare_history:

_indexperf_datebase_100daily_returnrolling_30d_returnytd_return
euro_stoxx_502026-04-13204.269001386657-0.0038324561027185972-0.044198430631456675-0.007650875149683767
stoxx_asia_502026-04-13202.149163828113070.0008520751196449025-0.0122980002503848550.08898338798884886
stoxx_usa_502026-04-13281.948420221825530.00445219831699007140.00033623782369995860.041408488989996384
euro_stoxx_502026-04-10205.054865156016320.0020284551930983686-0.037822516443515086-0.0038331092699792624
stoxx_asia_502026-04-10201.977064196971950.0014575875829731543-0.0261766590825539550.088056282301926192

Mart-build failure and resolution

The first factsheet build failed because the query used a correlated subquery pattern that BigQuery could not de-correlate.

Observed planner error:

Correlated subqueries that reference other tables are not supported unless they can be de-correlated...

The final build rewrote the logic into explicit pre-aggregated CTEs and ARRAY_AGG(...) constructs, which restored stable set-based joins.

Firestore serving publication

The serving publication step materializes the BigQuery marts into Firestore documents optimized for low-latency application reads rather than analytical scans.

  • top-level collection: stoxx_indices
  • one root document per index
  • subcollections:
    • constituents
    • sectors
    • performance
  • publish-complete control document:
    • serving_control/current

Inspect the Firestore control document

Reads the Firestore control document that signals the completion of the latest serving publish.

from google.cloud import firestore
 
db = firestore.Client(project="bq-wh-nb", database="main")
snap = db.collection("serving_control").document("current").get()
print(snap.to_dict())

Returned control document:

Returns the Firestore control payload that marks the serving refresh boundary.

{
  "pipeline": "stoxx_stage_yfinance",
  "root_doc_count": 3,
  "updated_at": "2026-04-13 18:14:46.203000+00:00",
  "publisher": "stoxx-serving",
  "publish_id": "20260413T181446.198512Z",
  "database": "main",
  "constituent_doc_count": 150,
  "published_at": "2026-04-13 18:14:46.203000+00:00",
  "indexes": [
    "euro_stoxx_50",
    "stoxx_asia_50",
    "stoxx_usa_50"
  ],
  "sector_doc_count": 28,
  "performance_doc_count": 4044
}

Inspect a serving subcollection

Reads the first five constituent documents for euro_stoxx_50 ordered by composite_rank from Firestore.

from google.cloud import firestore
 
db = firestore.Client(project="bq-wh-nb", database="main")
docs = (
    db.collection("stoxx_indices")
      .document("euro_stoxx_50")
      .collection("constituents")
      .order_by("composite_rank")
      .limit(5)
      .stream()
)
for doc in docs:
    print(doc.to_dict())

Sample from stoxx_indices/euro_stoxx_50/constituents:

symbolscore_datecomposite_scorecomposite_ranksector
BNP.PA2026-04-130.65027023641565261Financial Services
VOW.DE2026-04-130.46685499818997212Consumer Cyclical
IFX.DE2026-04-130.43697459334179813Technology
TTE.PA2026-04-130.43182062306397524Energy
ENI.MI2026-04-130.42199736537515345Energy

Inspect the root serving document

Reads the root Firestore document for euro_stoxx_50 to show the compact factsheet-style serving shape published from BigQuery marts.

from google.cloud import firestore
 
db = firestore.Client(project="bq-wh-nb", database="main")
snap = db.collection("stoxx_indices").document("euro_stoxx_50").get()
print(snap.to_dict())

Sample from the root serving document stoxx_indices/euro_stoxx_50:

Returns the compact Firestore root document consumed by the Euro STOXX 50 factsheet view.

{
  "_index": "euro_stoxx_50",
  "as_of_date": "2026-04-13",
  "stocks_count": 50,
  "avg_composite_score": 0.01923119403749058,
  "avg_market_cap": 102041376440,
  "top_constituents": [
    {
      "symbol": "BNP.PA",
      "composite_rank": 1,
      "sector": "Financial Services"
    },
    {
      "symbol": "VOW.DE",
      "composite_rank": 2,
      "sector": "Consumer Cyclical"
    },
    {
      "symbol": "IFX.DE",
      "composite_rank": 3,
      "sector": "Technology"
    }
  ]
}

Eventarc notification on control-document publication

At the end of the Firestore publication step, the pipeline updates a single control document. Eventarc listens to serving_control/current in Firestore database main and routes each matching google.cloud.firestore.document.v1.written event to the Cloud Run receiver service.

Inspect the latest CloudEvent summary

Reads the latest receiver log line from Cloud Logging to inspect the Firestore CloudEvent metadata emitted through Eventarc.

gcloud logging read "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"stoxx-firestore-eventarc-receiver\" AND textPayload:\"Firestore control-doc event received\"" \
  --project=bq-wh-nb \
  --limit=1 \
  --freshness=24h

Latest observed CloudEvent in Cloud Logging:

fieldvalue
ce_idc45b216f-7ff8-4364-bf7a-8ce21b3a7857
ce_typegoogle.cloud.firestore.document.v1.written
ce_subjectdocuments/serving_control/current
ce_time2026-04-13T18:14:46.225695Z
ce_source//firestore.googleapis.com/projects/bq-wh-nb/databases/main
body_size_bytes967

Inspect the raw receiver log line

Prints the raw Cloud Run receiver log entry so the full Firestore CloudEvent payload shape is visible.

2026-04-13 18:14:46,360 INFO firestore-control-eventarc-receiver {"body_size_bytes": 967, "ce_id": "c45b216f-7ff8-4364-bf7a-8ce21b3a7857", "ce_source": "//firestore.googleapis.com/projects/bq-wh-nb/databases/main", "ce_subject": "documents/serving_control/current", "ce_time": "2026-04-13T18:14:46.225695Z", "ce_type": "google.cloud.firestore.document.v1.written", ... "message": "Firestore control-doc event received", ...}

The receiver log line confirms that the Firestore publication boundary emitted a downstream event. The same control-document write can drive a dashboard refresh, cache invalidation workflow, or notification path.

Source files

The embedded scripts, SQL files, payload samples, and helper files referenced by this note now live in 02-stoxx-index-pipeline-source-files. Keeping the artifact bundle in a sibling note preserves the operational walkthrough here while retaining the full reference set for audit and reuse.