Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions workshop_public/RTA-mini-workshop/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Secrets — never commit real connection details
.env
dashboard/.env
**/.env

# Python
__pycache__/
*.pyc
*.pyo
.venv/
venv/
env/

# Pages build output
_site/

# OS / editor cruft
.DS_Store
Thumbs.db
.idea/
.vscode/
40 changes: 40 additions & 0 deletions workshop_public/RTA-mini-workshop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# RTA Mini Workshop — Real-Time Market Analytics with ClickHouse

A ~90-minute, self-contained hands-on lab: participants sign up for ClickHouse
Cloud, load ~26.5M forex ticks from public object storage, run real
market-analytics queries, try the built-in AI Assistant and Agents, and
(optionally) run a live dashboard on their own data. Generic and partner-neutral.

## Where the pieces live

The workshops are served by the workshop site, so the **guide** is served from
there and only the **dashboard** (a separate service) lives here:

| Piece | Location | Served at |
|---|---|---|
| Participant guide (MDX docs tree) | `site/content/docs/rta-mini/` | `https://labs.demohouse.cloud/docs/rta-mini` |
| Optional live dashboard | `dashboard/` (this folder) | run locally with Docker — see `dashboard/README.md` |

The guide is linked from the workshops hub (the app's `/` route).

## The workshop data

The guide loads a public forex Parquet dataset via a ClickPipe and via a one-shot
`s3(...)` query:

```
https://partner-workshop.s3.ap-southeast-1.amazonaws.com/fx/ticks.parquet
```

> **Note.** The dataset lives in a neutral, public S3 bucket (`partner-workshop`
> in `ap-southeast-1`), so the lab is fully partner-neutral and works with both
> the ClickPipes S3 source and the `s3()` function (anonymous `GetObject` +
> `ListBucket`). To host your own copy, upload `fx/ticks.parquet` to a public
> bucket and update the two references in the guide
> (`site/content/docs/rta-mini/learner/load-data.mdx`, served at
> `https://labs.demohouse.cloud/docs/rta-mini`).

## Prerequisites (participants)

- A ClickHouse Cloud account (free trial — created during the lab).
- For the optional dashboard: Docker Desktop.
7 changes: 7 additions & 0 deletions workshop_public/RTA-mini-workshop/dashboard/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.env
__pycache__/
*.pyc
.venv/
venv/
.git/
.DS_Store
20 changes: 20 additions & 0 deletions workshop_public/RTA-mini-workshop/dashboard/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copy this file to ".env" and fill in your ClickHouse Cloud connection details.
# Find them in the Cloud console: left menu -> Connect -> "HTTPS" / "Native".

# Host looks like: abc123xyz.ap-southeast-1.aws.clickhouse.cloud (no https://, no port)
CLICKHOUSE_HOST=your-service-id.ap-southeast-1.aws.clickhouse.cloud

# HTTPS port for ClickHouse Cloud is 8443.
CLICKHOUSE_PORT=8443

# Default user unless you created another one.
CLICKHOUSE_USER=default

# The password you set when you created the service.
CLICKHOUSE_PASSWORD=your-service-password

# Database that holds the `forex` table (usually "default").
CLICKHOUSE_DATABASE=default

# ClickHouse Cloud always uses TLS.
CLICKHOUSE_SECURE=true
16 changes: 16 additions & 0 deletions workshop_public/RTA-mini-workshop/dashboard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Pin the Python version here so it doesn't matter what's on your laptop.
FROM python:3.12-slim

WORKDIR /app

# Install dependencies first for better layer caching.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# App code + static front end.
COPY app.py .
COPY static ./static

EXPOSE 8000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
69 changes: 69 additions & 0 deletions workshop_public/RTA-mini-workshop/dashboard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Forex Live Dashboard

A small dashboard that visualizes the `forex` table you loaded in the workshop,
served from your own ClickHouse Cloud service. Change a filter and watch the
query time — that's the point: ClickHouse answers in milliseconds.

- **Backend:** FastAPI + `clickhouse-connect`
- **Charts:** Apache ECharts (candlestick + volume, interactive zoom)
- **Runs in Docker** so your laptop's Python version doesn't matter.

## Prerequisites

- [Docker Desktop](https://www.docker.com/products/docker-desktop/) (includes `docker compose`).
- A running ClickHouse Cloud service with the `forex` table loaded (Steps 1–3 of the workshop).

## Setup

1. Clone the repo and go into this folder:

```bash
git clone https://github.com/ClickHouse/ClickHouse_Demos.git
cd ClickHouse_Demos
cd "$(git rev-parse --show-toplevel)/workshop_public/RTA-mini-workshop/dashboard"
```

2. Create your `.env` from the template and fill in your connection details:

```bash
cp .env.example .env
```

Find the values in the Cloud console under **Connect → HTTPS/Native**:

| Variable | Value |
| --- | --- |
| `CLICKHOUSE_HOST` | e.g. `abc123.ap-southeast-1.aws.clickhouse.cloud` (no `https://`, no port) |
| `CLICKHOUSE_PORT` | `8443` |
| `CLICKHOUSE_USER` | `default` |
| `CLICKHOUSE_PASSWORD` | the password you set when creating the service |
| `CLICKHOUSE_DATABASE` | `default` |
| `CLICKHOUSE_SECURE` | `true` |

3. Build and run:

```bash
docker compose up --build
```

4. Open <http://localhost:8000>.

Stop it with `Ctrl-C`, or `docker compose down` from another terminal.

## Run without Docker (optional)

Needs Python 3.9+:

```bash
pip install -r requirements.txt
cp .env.example .env # then edit it
uvicorn app:app --reload --port 8000
```

## Troubleshooting

- **Red banner "Could not reach ClickHouse":** re-check the values in `.env`. The
host must have no `https://` prefix and no port suffix; the port is `8443`.
- **"forex table not found":** load the data first (workshop Steps 2–3).
- **Port 8000 in use:** change the mapping in `docker-compose.yml`, e.g.
`"8080:8000"`, then open <http://localhost:8080>.
198 changes: 198 additions & 0 deletions workshop_public/RTA-mini-workshop/dashboard/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"""
ClickHouse Forex Live Dashboard — FastAPI backend.

Serves a small single-page dashboard (static/) and a JSON API that queries the
`forex` table you loaded during the workshop. Every API response carries the
ClickHouse query timing and rows-scanned count, so the front end can show how
little work the database does when a filter hits the sort key.

Runs on Python 3.9+ (the Docker image pins 3.12, so your laptop's Python
version doesn't matter).
"""

import os
import time
from datetime import datetime, timedelta

from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles

# Load a local .env if present (Docker passes these via env_file instead).
try:
from dotenv import load_dotenv

load_dotenv()
except Exception: # python-dotenv is optional at runtime
pass

import clickhouse_connect

app = FastAPI(title="ClickHouse Forex Live Dashboard")

_client = None


def get_client():
"""Create (once) and reuse a ClickHouse Cloud client from env vars."""
global _client
if _client is None:
host = os.getenv("CLICKHOUSE_HOST")
if not host:
raise HTTPException(
status_code=500,
detail="CLICKHOUSE_HOST is not set. Copy .env.example to .env "
"and fill in your ClickHouse Cloud connection details.",
)
_client = clickhouse_connect.get_client(
host=host,
port=int(os.getenv("CLICKHOUSE_PORT", "8443")),
username=os.getenv("CLICKHOUSE_USER", "default"),
password=os.getenv("CLICKHOUSE_PASSWORD", ""),
database=os.getenv("CLICKHOUSE_DATABASE", "default"),
secure=os.getenv("CLICKHOUSE_SECURE", "true").lower() in ("1", "true", "yes"),
connect_timeout=15,
query_limit=0,
)
return _client


def _run(sql, params=None):
"""Run a query and return (result, wall_ms, rows_read, server_ms)."""
client = get_client()
t0 = time.perf_counter()
res = client.query(sql, parameters=params or {})
wall_ms = (time.perf_counter() - t0) * 1000.0
summary = res.summary or {}
rows_read = int(summary.get("read_rows", 0) or 0)
elapsed_ns = summary.get("elapsed_ns")
server_ms = (int(elapsed_ns) / 1_000_000.0) if elapsed_ns else None
return res, wall_ms, rows_read, server_ms


@app.get("/api/health")
def health():
try:
_run("SELECT 1")
return {"ok": True}
except Exception as exc: # surface config/connection problems to the UI
return JSONResponse(status_code=500, content={"ok": False, "error": str(exc)})


@app.get("/api/meta")
def meta():
"""Currency pairs and the available date range, for the filter controls."""
res, _, _, _ = _run(
"SELECT DISTINCT concat(base, '/', quote) AS pair FROM forex ORDER BY pair"
)
pairs = [row[0] for row in res.result_rows]
res2, _, _, _ = _run(
"SELECT toDate(min(datetime)) AS a, toDate(max(datetime)) AS b FROM forex"
)
dmin, dmax = res2.result_rows[0]
return {"pairs": pairs, "date_min": str(dmin), "date_max": str(dmax)}


@app.get("/api/dashboard")
def dashboard(
pair: str = Query(..., description="e.g. XAU/USD"),
start: str = Query(..., description="YYYY-MM-DD (inclusive)"),
end: str = Query(..., description="YYYY-MM-DD (inclusive)"),
bucket: str = Query("day", pattern="^(day|hour)$"),
):
"""OHLC candles + volume + KPIs for one pair over a date range."""
if "/" not in pair:
raise HTTPException(400, "pair must look like BASE/QUOTE, e.g. XAU/USD")
base, quote = pair.split("/", 1)

try:
start_date = datetime.strptime(start, "%Y-%m-%d")
end_date = datetime.strptime(end, "%Y-%m-%d")
if start_date > end_date:
raise HTTPException(400, "start must be on or before end")
start_dt = start_date.strftime("%Y-%m-%d %H:%M:%S")
# end is inclusive, so we scan up to the start of the following day
end_dt = (end_date + timedelta(days=1)).strftime(
"%Y-%m-%d %H:%M:%S"
)
except ValueError:
raise HTTPException(400, "start and end must be YYYY-MM-DD")

bucket_expr = "toStartOfHour(datetime)" if bucket == "hour" else "toStartOfDay(datetime)"
params = {"base": base, "quote": quote, "start": start_dt, "end": end_dt}
where = (
"base = {base:String} AND quote = {quote:String} "
"AND datetime >= {start:DateTime} AND datetime < {end:DateTime}"
)

ohlc_sql = f"""
SELECT {bucket_expr} AS t,
argMin(bid, datetime) AS open,
max(bid) AS high,
min(bid) AS low,
argMax(bid, datetime) AS close,
count() AS volume,
round(avg(ask - bid), 6) AS avg_spread
FROM forex
WHERE {where}
GROUP BY t
ORDER BY t
"""
kpi_sql = f"""
SELECT count() AS ticks,
round(quantile(0.5)(ask - bid), 6) AS median_spread,
round(quantile(0.99)(ask - bid), 6) AS p99_spread,
argMax(bid, datetime) AS last_bid,
min(bid) AS low,
max(bid) AS high
FROM forex
WHERE {where}
"""

r1, w1, rr1, s1 = _run(ohlc_sql, params)
r2, w2, rr2, s2 = _run(kpi_sql, params)

ohlc = [
{
"t": str(row[0]),
"open": row[1],
"high": row[2],
"low": row[3],
"close": row[4],
"volume": int(row[5]),
"spread": row[6],
}
for row in r1.result_rows
]

if r2.result_rows and r2.result_rows[0][0]:
k = r2.result_rows[0]
kpis = {
"ticks": int(k[0]),
"median_spread": k[1],
"p99_spread": k[2],
"last_bid": k[3],
"low": k[4],
"high": k[5],
}
else:
kpis = {"ticks": 0, "median_spread": None, "p99_spread": None,
"last_bid": None, "low": None, "high": None}

server_ms = round(s1 + s2, 2) if (s1 is not None and s2 is not None) else None
timing = {
"server_ms": server_ms,
"wall_ms": round(w1 + w2, 2),
"rows_read": rr1 + rr2,
"queries": 2,
}
return {"ohlc": ohlc, "kpis": kpis, "timing": timing, "bucket": bucket, "pair": pair}


# Static single-page front end. Mount last so /api/* routes win.
app.mount("/static", StaticFiles(directory="static"), name="static")


@app.get("/")
def index():
return FileResponse("static/index.html")
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
dashboard:
build: .
ports:
- "8000:8000"
env_file:
- .env
restart: unless-stopped
4 changes: 4 additions & 0 deletions workshop_public/RTA-mini-workshop/dashboard/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fastapi>=0.111,<1.0
uvicorn[standard]>=0.30,<1.0
clickhouse-connect>=0.8.0,<1.0
python-dotenv>=1.0,<2.0
Loading
Loading