Key Points 5 min read
  • Complete MLOps lifecycle: data collection → feature engineering → training → experiment tracking → real-time inference → deployment → monitoring.
  • raw_db (PostgreSQL) stores raw data, ClickHouse stores cleaned OLAP data, and Prefect 2 handles ETL and training scheduling.
  • Evidently exports drift metrics to Prometheus, visualized with Grafana; Kafka + WebSocket powers real-time predictions and metric pushing.
Table of Contents

Models trained in a machine learning course usually stay stuck in a Jupyter Notebook, lacking version control, scheduled retraining, and monitoring. Stock MLOps is a course project that uses “stock price prediction” as its vehicle, with a clear goal: put everything learned in the course to use and build an end-to-end ML system with a complete MLOps workflow.

The system is meant to be a sustainable, maintainable stock price prediction service, covering the full lifecycle from data collection, feature engineering, model training, experiment tracking, and real-time inference to deployment and monitoring. Users can query predicted stock prices and historical trend charts through a web interface; developers can periodically retrain models, track experiments, monitor performance and data drift, and trigger automatic retraining.

Technology Choices

The entire system is orchestrated on a single machine with Docker Compose (extensible to EC2), with cleanly separated responsibilities across layers:

CategoryTools
Cloud / InfraDocker Compose, MinIO, PostgreSQL, ClickHouse
ML PipelineFastAPI, Scikit-learn, Pandas, MLflow
Workflow OrchestrationPrefect 2
MonitoringEvidently + Prometheus + Grafana
CI/CDGitHub Actions
Testingpytest (unit + integration)
Formatting / Hooksblack, pre-commit, flake8
IaCDocker Compose + Volume + Network (extensible to Terraform)

The frontend is Vite + React, the backend is several FastAPI containers, and Nginx sits in front to serve static files and act as a reverse proxy.

Data Layering: PostgreSQL Raw Store + ClickHouse OLAP

This project applies clear layering at the data tier, rather than cramming everything into a single database:

  • raw_db (PostgreSQL): stores the raw data pulled in by ETL
  • ClickHouse: stores the cleaned data, serving as the OLAP query layer — both inference and training read features from here

The data source is historical Taiwan/US stock data from Yahoo Finance (e.g. 2330.TW, AAPL, TSM), which after ETL transformation is landed in Parquet format (workflows/parquet/).

Model Lifecycle

The entire model lifecycle is strung together by Prefect:

  1. The ETL and training pipelines are triggered periodically by Prefect 2
  2. Training results are logged to MLflow and registered as versioned models
  3. FastAPI provides the /predict and /train APIs (backed by Celery for async tasks)
  4. Evidently exports model drift metrics to Prometheus
  5. Grafana dashboards visualize prediction accuracy, drift metrics, and system metrics

MLflow here itself uses multiple database roles: mlflow-db (PostgreSQL) stores model metadata, there’s a separate internal MLflow DB, and model artifacts are stored in MinIO.

System Architecture

Nginx is more than a plain reverse proxy — it splits traffic to different upstream pools based on routing and performs weighted load balancing:

  • backend_predict: 70% to backend1, 30% to backend2
  • backend_train: 30% to backend1, 70% to backend2
  • backend_api: backend1 and backend2 evenly (1:1)
  • /ws: routed to the WebSocket monitoring service
graph TD
  U[User Browser] -->|HTTP / WS| NG[Nginx<br>Static + Reverse Proxy]

  NG -->|/api/predict| UP1[backend_predict<br>70/30]
  NG -->|/api/train| UP2[backend_train<br>30/70]
  NG -->|/api/| UP3[backend_api<br>1:1]
  NG -->|/ws| W[ws_monitor<br>Kafka Consumer + WebSocket]
  NG -->|Static| Static[React Build]

  UP1 --> B1[backend1:8000]
  UP1 --> B2[backend2:8000]
  UP2 --> B1
  UP2 --> B2
  UP3 --> B1
  UP3 --> B2

  subgraph ETL
    P[Prefect Workflow] -->|raw| D1[(raw_db<br>PostgreSQL)]
    P -->|cleaned| D2[("ClickHouse<br>OLAP")]
  end

  B1 & B2 -->|Query features| D2
  B1 & B2 -->|Push task| E[(Redis)]

  subgraph Training
    E -->|Execute| L[Celery Worker]
    L -->|Read| D2
    L -->|Track| H[MLflow Registry]
    H -->|Artifact| S[(MinIO)]
    H --> D3[(mlflow-db<br>PostgreSQL)]
  end

  subgraph Monitoring
    M[Evidently] --> J[Prometheus]
    J --> K[Grafana Dashboard]
    Q[metrics_publisher<br>push every 5s] --> KAF[Kafka]
    KAF --> W
  end

Real-Time Pushing: Kafka + WebSocket

Monitoring isn’t just an offline dashboard. The system uses Kafka for real-time data streaming, paired with two dedicated services:

  • metrics_publisher: fetches metrics every 5 seconds and sends them to Kafka’s metrics topic
  • ws_monitor: acts as a Kafka consumer, and at the same time uses WebSocket to push predictions from the prediction topic and metrics from the metrics topic to the frontend in real time

This lets both “prediction results” and “system/model metrics” reflect on the interface in near real time, without polling.

Engineering Practices and CI/CD

This project rounds out reproducibility and engineering discipline quite thoroughly:

  • Testing: pytest covers both unit tests (test_train.py, test_predict.py) and integration tests (predict / train APIs)
  • Formatting and hooks: black, flake8, paired with a pre-commit config
  • Automation: a Makefile (make dev-setup, make train, make workflow) keeps the environment and workflow consistent
  • CI/CD: GitHub Actions runs CI (ci-tests.yml) and CD deployment (cd-deploy.yml), and sends Discord notifications via webhook

Getting it up and running is also straightforward:

python -m venv .venv
source .venv/bin/activate
pip install -r backend/requirements.txt

docker compose up --build

make train      # one-off training
make workflow   # run the Prefect workflow

Conclusion

The value of Stock MLOps lies not in the accuracy of the stock price prediction itself, but in how it wires up a genuinely maintainable ML pipeline from end to end: data layering (PostgreSQL raw_db → ClickHouse OLAP), Prefect scheduling, MLflow version management, FastAPI + Celery servitization, Evidently/Prometheus/Grafana monitoring, and finally Kafka/WebSocket real-time pushing and GitHub Actions CI/CD. For anyone looking to turn a “model in a Notebook” into a “production-grade system,” this is a clearly structured reference skeleton.

References

Ask this article

Answers come from this article only. Click any prompt below or open the chat at the bottom right.

🇺🇸 English

Ever trained a machine learning model in a class, watched it hit decent accuracy, and then... just left it sitting there in a Jupyter Notebook? No version control, no scheduled retraining, no monitoring — a model frozen in amber. That's the exact problem this project sets out to solve. It's called Stock MLOps, and the subject it predicts — stock prices — is honestly beside the point. The real goal is to take everything you'd learn in an MLOps course and wire it together into one living, breathing, end-to-end system.

So what does "end-to-end" actually mean here? Picture the full lifecycle: pulling in data, engineering features, training the model, tracking experiments, serving real-time predictions, and then deploying and monitoring the whole thing. From the outside, a user just visits a web page, types in a stock, and sees a predicted price plus a historical trend chart. But behind that simple page, a developer can retrain models on a schedule, compare experiments, watch for performance decay and data drift, and even trigger automatic retraining when things start to slip.

Here's the fun constraint: all of this runs on a single machine, orchestrated with Docker Compose. Not a sprawling cloud cluster — one box, cleanly divided into layers, and designed so it could be lifted onto an EC2 instance later without rethinking everything.

Let's talk about the stack, but in terms of jobs rather than logos. For infrastructure, Docker Compose ties it together, MinIO handles object storage, and there are two databases — PostgreSQL and ClickHouse — which we'll come back to, because how they're used is one of the more interesting design decisions. For the ML pipeline itself: FastAPI serves the APIs, Scikit-learn and Pandas do the modeling and data wrangling, and MLflow tracks experiments. Prefect 2 orchestrates the workflows. Monitoring is a trio — Evidently for drift detection, Prometheus for metrics collection, Grafana for the dashboards. And on the engineering side, GitHub Actions runs CI/CD, pytest handles testing, and black, flake8, and pre-commit keep the code clean. The frontend is Vite and React, the backend is a handful of FastAPI containers, and Nginx sits out front serving static files and acting as a reverse proxy.

Now, that data layering I promised. A lot of student projects cram everything into one database and call it a day. This one deliberately splits it. There's a raw store — that's PostgreSQL, and it holds exactly what the ETL process pulls in, untouched. Then there's ClickHouse, which holds the cleaned, transformed data and acts as the analytical query layer. The reason that matters: ClickHouse is built for OLAP — fast analytical reads over big columns of data — so both inference and training pull their features from there, while PostgreSQL keeps the messy original safe. The data itself comes from Yahoo Finance — historical prices for Taiwan and US stocks, things like TSMC and Apple — and after transformation it lands as Parquet files, a compact columnar format that's cheap to read.

So how does the model actually stay alive over time? Prefect is the conductor. On a schedule, it kicks off the ETL and training pipelines. When training finishes, the results get logged to MLflow and registered as a versioned model — so you always know which model is which, and you can roll back. FastAPI exposes two key endpoints: one to predict, one to train, with Celery handling the training as an async background task so nobody's HTTP request hangs while a model trains. Meanwhile Evidently is computing drift metrics and pushing them into Prometheus, and Grafana turns all of it — prediction accuracy, drift, system health — into dashboards you can actually watch.

One nice detail: MLflow here wears several hats at the data level. A PostgreSQL database stores the model metadata, there's a separate internal database for MLflow's own bookkeeping, and the actual model artifacts — the trained files themselves — live in MinIO object storage. Metadata in a database, big binary blobs in object storage. That's exactly the separation you want.

Let's zoom in on Nginx, because it's doing more than the usual reverse-proxy job. It's actually splitting traffic across two backend instances with different weightings depending on what kind of request comes in. Prediction traffic leans heavily on backend one — about seventy percent there, thirty percent to backend two. Training traffic flips that: mostly backend two. General API calls get split evenly, fifty-fifty. And any WebSocket connection gets routed to a dedicated monitoring service. So it's not just load balancing — it's role-aware load balancing, sending different workloads to different places.

Let me walk you through the whole flow as one picture. A user's browser talks to Nginx over HTTP or WebSocket. Nginx serves the React build for static content, and routes API calls to those two weighted backend pools. Off to the side, Prefect runs the ETL — dropping raw data into PostgreSQL and cleaned data into ClickHouse. The backends query their features from ClickHouse and, when a training job comes in, push a task onto Redis. A Celery worker picks that task up, reads from ClickHouse, trains the model, tracks it in the MLflow registry — which stores artifacts in MinIO and metadata in PostgreSQL. And the monitoring corner has Evidently feeding Prometheus feeding Grafana, plus a metrics publisher pushing updates into Kafka every five seconds. Everything has its lane.

That Kafka piece leads to my favorite part: real-time push. Monitoring here isn't a dashboard you refresh and wait on. The system streams data live through Kafka with two dedicated services. One, the metrics publisher, grabs metrics every five seconds and drops them onto a Kafka topic. The other, the WebSocket monitor, consumes from Kafka and simultaneously pushes both predictions and metrics out to the frontend over WebSocket. The payoff is that prediction results and system metrics show up on the screen almost instantly — no polling, no hammering the server with "anything new yet? anything new yet?"

And then there's the engineering discipline, which is what separates a demo from something maintainable. There's real testing — pytest covers unit tests for training and prediction, plus integration tests against the actual predict and train APIs. Formatting and hooks are enforced with black, flake8, and pre-commit, so bad style never even reaches the repo. A Makefile wraps the common commands so setup and workflows stay consistent across machines. And GitHub Actions runs the full CI and CD pipeline, even firing a Discord notification through a webhook when something deploys. Getting it running yourself is refreshingly boring, in the good way: spin up a virtual environment, install the requirements, run docker compose up to build everything, and then a single make command to train a model or kick off the Prefect workflow.

So let me leave you with the three things that actually matter here. First: the value was never the stock prediction accuracy — it's the wiring. This project takes a model that would've died in a notebook and gives it a full production nervous system. Second: notice the recurring pattern of clean separation — raw data versus cleaned data, metadata versus artifacts, prediction traffic versus training traffic. Good MLOps is mostly about drawing the right boundaries. And third: the lifecycle only closes because of feedback loops — Prefect schedules retraining, Evidently watches for drift, and monitoring flows back to trigger the next round. That loop is what makes a system sustainable instead of a one-time experiment. If you've got a model stuck in a notebook and you're wondering what "production-grade" really looks like, this is a clean skeleton to study.

🇹🇼 中文

機器學習課程裡訓練好的模型,多半就停在 Jupyter Notebook 裡,沒有版本管理、沒有排程重訓、也沒有監控。今天要聊的這個叫 Stock MLOps 的課程專案,就是拿「股價預測」當載體,把課堂上學到的東西全部串起來,做成一套帶完整 MLOps 工作流的端到端系統。

它的目標很明確:做一個可持續、可維護的股價預測服務,涵蓋資料蒐集、特徵工程、模型訓練、實驗追蹤、即時推理,一路到部署跟監控的完整生命週期。使用者這一端,可以透過網頁介面查預測股價、看歷史走勢;開發者這一端,則能定期重訓模型、追蹤實驗、監控效能跟資料漂移,還能觸發自動重訓。

先講技術選型。整套系統是用 Docker Compose 在單機上編排的,之後也可以延伸到 EC2。各層的職責切得很乾淨:雲端跟基礎設施用 Docker Compose、MinIO、PostgreSQL 加 ClickHouse;ML pipeline 這一層是 FastAPI、Scikit-learn、Pandas 搭 MLflow;工作流編排交給 Prefect 2;監控用 Evidently 加 Prometheus 加 Grafana;CI/CD 走 GitHub Actions;測試用 pytest,涵蓋單元測試跟整合測試;格式化跟 hooks 則是 black、flake8 配 pre-commit。前端是 Vite 加 React,後端是好幾個 FastAPI 容器,前面再掛一個 Nginx 做靜態檔案服務跟反向代理。

接著是這個專案我覺得做得蠻漂亮的地方,資料分層。它沒有把所有東西塞進同一個資料庫,而是明確分開:PostgreSQL 當原始庫,也就是 raw_db,存 ETL 抓進來的原始資料;ClickHouse 則存清洗過後的資料,當 OLAP 查詢層,之後推理跟訓練都是從 ClickHouse 讀特徵。資料來源是 Yahoo Finance 的台股、美股歷史資料,像是台積電、蘋果這些,經過 ETL 轉換之後,用 Parquet 格式落地。

模型的生命週期,是由 Prefect 串起來的。流程大概是這樣:ETL 跟訓練 pipeline 由 Prefect 2 定期觸發;訓練結果記錄到 MLflow,並且註冊成有版本的模型;FastAPI 對外提供 predict 跟 train 兩個 API,背後由 Celery 支援非同步任務;Evidently 把模型漂移的指標輸出到 Prometheus;最後 Grafana 的儀表板把預測準確度、漂移指標跟系統指標都視覺化出來。這裡值得一提的是,MLflow 本身就用到多個資料庫角色,模型的 metadata 存在一個 PostgreSQL,模型的 artifact 則是存進 MinIO。

再來看系統架構,特別是 Nginx 的角色。它不只是單純的反向代理,而是依照路由把流量分流到不同的後端池,還帶了權重做負載均衡。舉例來說,預測的請求是七比三,七成給 backend1、三成給 backend2;訓練的請求反過來,三比七;一般 API 就是各半、一比一;至於 WebSocket 的路由,會導向專門的監控服務。

即時推送這塊也很有意思。監控不是只有離線看板,系統用 Kafka 做即時資料流,搭配兩個專門的服務。一個叫 metrics_publisher,每五秒抓一次指標,發送到 Kafka 的 metrics topic。另一個叫 ws_monitor,它同時是 Kafka 的 consumer,又透過 WebSocket 把預測結果跟指標即時推到前端。這樣一來,預測結果跟系統、模型的指標,都能近乎即時地反映在介面上,而不用一直輪詢。

工程實踐跟 CI/CD 也補得相當完整。測試用 pytest,單元測試涵蓋訓練跟預測,整合測試則測 predict 跟 train 這兩個 API。格式有 black、flake8 加 pre-commit 把關。自動化用 Makefile,讓環境設定、訓練、跑 workflow 這些流程都保持一致。CI/CD 是 GitHub Actions 跑測試跟部署,並且透過 webhook 發 Discord 通知。啟動方式其實也很單純,建好虛擬環境、裝好依賴,一行 docker compose up --build 就把整套拉起來,訓練跟 workflow 各有對應的 make 指令。

最後收個尾。這個專案真正的價值,我認為有三個重點。第一,它的重點不在股價預測的精度,而在於把一條真正可維護的 ML pipeline 從頭到尾完整串起來。第二,資料分層的思路很值得學,用 PostgreSQL 當原始庫、ClickHouse 當 OLAP 查詢層,職責分明。第三,它示範了怎麼把 Notebook 裡的模型,變成一個有排程、有版本、有監控、有即時推送的生產級系統。對想跨過這一步的人來說,這是一份結構很清楚的參考骨架。

Tags

Related Articles

Live English Tutor: Building a Real-Time Voice AI English Tutor with LiveKit + Gemini Native Audio

A real-time-voice-first AI English tutoring system: students converse with the AI teacher Emma via microphone (optionally with video/screen sharing), the system corrects mistakes in real time, and generates a post-class report in Chinese. The technical core is LiveKit (Self-hosted WebRTC) + Google Gemini 2.5 Flash Native Audio, with a FastAPI backend handling auth, courses, and data persistence.