Skip to content
Open
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ wheels/
venv/
ENV/

# uv
packages/*/uv.lock

# Testing
.pytest_cache/
.coverage
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Interactive 16-step × 8-instrument beat sequencer with click-to-toggle beats, a
### Particle Sandbox (`demo-particles`)
Real-time physics simulation with click-to-spawn particles, adjustable gravity/friction/bounce, and attractor/repulsor particle types.

### Crypto Stream (`demo-crypto-stream`)
Live cryptocurrency dashboard streaming real-time prices for 10 trading pairs from Binance. Click any card to open a 48h candlestick chart that keeps the latest candle updating live via a second WebSocket stream.

![Crypto Stream Demo](packages/demo-crypto-stream/crypto.gif)

## Requirements

- Python 3.13
Expand Down Expand Up @@ -53,6 +58,7 @@ just nuclear # Nuclear reactor simulator
just gol # Game of Life
just drums # Drum machine
just particles # Particle sandbox
just crypto # Crypto stream dashboard
```

Or directly:
Expand All @@ -63,6 +69,7 @@ uv run demo-nuclear
uv run demo-gol
uv run demo-drums
uv run demo-particles
uv run demo-crypto-stream
```

## Project Structure
Expand All @@ -74,7 +81,8 @@ dash-ws-demo/
│ ├── demo-nuclear/ # Nuclear reactor simulator
│ ├── demo-game-of-life/ # Conway's Game of Life
│ ├── demo-drums/ # Drum machine
│ └── demo-particles/ # Particle sandbox
│ ├── demo-particles/ # Particle sandbox
│ └── demo-crypto-stream/ # Crypto stream dashboard
├── pyproject.toml # UV workspace config
├── uv.lock # Dependency lock file
└── justfile # Task automation
Expand Down
4 changes: 4 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ drums:
particles:
uv run demo-particles

# Run crypto stream demo
crypto:
uv run demo-crypto-stream

# Sync all packages
setup:
uv sync --all-packages
35 changes: 35 additions & 0 deletions packages/demo-crypto-stream/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# dash-crypto-stream

![demo](crypto.gif)

A live cryptocurrency dashboard built with Dash 4.2, demonstrating persistent WebSocket callbacks.

Streams real-time prices for 10 trading pairs from Binance. Click any card to open a candlestick chart that loads 48 hours of historical data and keeps the latest candle live via a second WebSocket stream.

## Features

- 10 live price cards (BTC, ETH, BNB, SOL, XRP, ADA, DOGE, AVAX, DOT, POL) updated via a single Binance combined stream
- 24h price change with green/red colouring
- Click any card to open a 48h candlestick chart
- Chart's latest candle updates live via `@kline_1h` WebSocket — no polling

## How it works

Two persistent callbacks run concurrently:

- `stream_all_tickers` — connects to Binance's combined stream (`/stream?streams=...`) and pushes price/change updates to all 10 cards via `set_props`
- `stream_klines` — monitors which card is open and subscribes to that symbol's `@kline_1h` stream, pushing figure updates to the chart via `set_props`

When a card is clicked, `toggle_modal` fetches 48 closed candles from the Binance REST API, renders the initial chart, then signals `stream_klines` to take over with live updates.

## Running

```bash
just crypto
```

Or directly:

```bash
uv run demo-crypto-stream
```
Binary file added packages/demo-crypto-stream/crypto.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Empty file.
179 changes: 179 additions & 0 deletions packages/demo-crypto-stream/demo_crypto_stream/assets/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
* { box-sizing: border-box; margin: 0; padding: 0; }

body {
background: #0f0f1a;
color: #e2e8f0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.app {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}

h1 {
font-size: 1.8rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: #f8fafc;
letter-spacing: -0.02em;
}

/* Card grid */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}

.card {
background: #1e1e2e;
border: 1px solid #2d2d3f;
border-radius: 12px;
padding: 1.25rem;
cursor: pointer;
transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
user-select: none;
}

.card:hover {
border-color: #4f46e5;
background: #252538;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(79, 70, 229, 0.18);
}

.card-header {
display: flex;
align-items: baseline;
gap: 2px;
margin-bottom: 0.6rem;
}

.card-name {
font-size: 1.1rem;
font-weight: 700;
color: #f8fafc;
}

.card-pair {
font-size: 0.7rem;
color: #475569;
}

.card-price {
font-size: 1.25rem;
font-weight: 600;
color: #e2e8f0;
font-variant-numeric: tabular-nums;
letter-spacing: -0.01em;
}

.card-change {
font-size: 0.85rem;
font-weight: 500;
margin-top: 0.2rem;
font-variant-numeric: tabular-nums;
}

.card-time {
font-size: 0.72rem;
color: #475569;
margin-top: 0.4rem;
font-variant-numeric: tabular-nums;
}

.loading {
color: #334155 !important;
animation: pulse 1.4s ease-in-out infinite;
}

@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}

/* Modal */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}

.modal-overlay.hidden {
display: none;
}

.modal-box {
background: #1e1e2e;
border: 1px solid #2d2d3f;
border-radius: 16px;
padding: 1.5rem;
width: min(92vw, 880px);
position: relative;
}

.modal-box h2 {
font-size: 1.1rem;
font-weight: 600;
color: #f8fafc;
margin-bottom: 1rem;
padding-right: 2.5rem;
}

.close-btn {
position: absolute;
top: 1rem;
right: 1rem;
background: #2d2d3f;
border: none;
color: #94a3b8;
width: 30px;
height: 30px;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
line-height: 1;
transition: background 0.15s, color 0.15s;
}

.close-btn:hover {
background: #3d3d52;
color: #f8fafc;
}

/* Chart loading overlay */
.chart-loading {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
border-radius: 12px;
pointer-events: none;
}

.chart-loading::after {
content: "";
width: 36px;
height: 36px;
border: 3px solid #2d2d3f;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 0.7s linear infinite;
}

.chart-loading.hidden {
display: none;
}

@keyframes spin {
to { transform: rotate(360deg); }
}
Loading