Analyze stock market data
Analyze financial tick and candlestick data with TimescaleDB using S&P 500 stock prices
This example demonstrates financial tick and candlestick data analysis with TimescaleDB. The dataset corresponds to the stocks listed in the S&P 500 index, with fictional prices and movements. The tick cadence is per second, over three business days, containing approximately 35 million records in total and 503 tickers.
If you would rather build candlestick charts on a live dashboard, see Visualize financial tick data with Grafana, which uses a crypto dataset.
Use cases
Section titled “Use cases”Trading platforms, market data analysis, portfolio analytics, algorithmic trading.
What you learn
Section titled “What you learn”- OHLCV (open, high, low, close, volume) data modeling.
- Candlestick aggregations at multiple intervals.
- continuous aggregates for different timeframes (1 minute, 5 minutes, 1 hour).
- Real-time market analysis queries.
Dataset preview
Section titled “Dataset preview”The dataset contains the following columns:
- timestamp
- ticker
- price
- price delta
- change percentage
- volume
2025-11-12 14:30:00+00:00,NVDA,38.25,0.25,0.65359,50957122025-11-12 14:30:00+00:00,AAPL,152.03,0.03,0.01973,64665542025-11-12 14:30:00+00:00,MSFT,129.23,0.23,0.17798,44178482025-11-12 14:30:00+00:00,GOOG,174.93,-0.07,-0.04002,196022292025-11-12 14:30:00+00:00,GOOGL,71.21,0.21,0.2949,31494822025-11-12 14:30:00+00:00,AMZN,95.95,-0.05,-0.05211,121504742025-11-12 14:30:00+00:00,AVGO,196.15,0.15,0.07647,61660472025-11-12 14:30:00+00:00,META,133.22,0.22,0.16514,122300042025-11-12 14:30:00+00:00,TSLA,82.0,0.0,0.0,102989372025-11-12 14:30:00+00:00,BRK.B,56.84,-0.16,-0.28149,10980047Prerequisites for this tutorial
To follow these steps, you'll need:
- Docker installed
- The
psqlPostgreSQL client
Set up and query the data
Section titled “Set up and query the data”First, download the dataset:
curl -L https://assets.timescale.com/timescaledb-datasets/sp500_stock_prices_3d_1s.tar.gz | tar -xzf -- Start TimescaleDB
Start TimescaleDB using one of these options.
The easiest way to get started:
ImportantThis script is intended for local development and testing only. Do not use it for production deployments. For production-ready installation options, see the TimescaleDB installation guide.
On Linux or Mac:
Terminal window curl -sL https://tsdb.co/start-local | shThis command:
- Downloads and starts TimescaleDB, if not already downloaded.
- Exposes PostgreSQL on port
6543, a non-standard port that avoids conflicts with other PostgreSQL instances on port 5432. - Automatically tunes settings for your environment using timescaledb-tune.
- Sets up a persistent data volume.
Run TimescaleDB directly with Docker. This option also works on Windows:
Terminal window docker run -d --name timescaledb \-p 6543:5432 \-e POSTGRES_PASSWORD=password \timescale/timescaledb-ha:pg18This example uses port
6543, mapped to container port 5432, to avoid conflicts if you have other PostgreSQL instances running on the standard port 5432.Wait about 1 to 2 minutes for TimescaleDB to download and initialize.
- Connect to TimescaleDB
Connect using
psql:Terminal window psql "postgres://postgres:password@localhost:6543/postgres"You should see the PostgreSQL prompt. Verify TimescaleDB is installed:
SELECT extname, extversion FROM pg_extension WHERE extname = 'timescaledb';Expected output:
extname | extversion-------------+------------timescaledb | 2.x.xTipsPrefer a GUI? If you would rather use a graphical tool instead of the command line, you can download pgAdmin and connect to TimescaleDB using the same connection details (host:
localhost, port:6543, user:postgres, password:password). - Create the schema
Create the optimized hypertable by running this SQL in your
psqlsession:-- Create the stock_prices table with the column ts as partitioning-- Note: for optimized query performance grouping on ticker, we select this column to segment byCREATE TABLE stock_prices (ts TIMESTAMPTZ NOT NULL,ticker TEXT NOT NULL,price DOUBLE PRECISION NOT NULL,change_delta DOUBLE PRECISION NOT NULL,change_percentage DOUBLE PRECISION NOT NULL,volume BIGINT NOT NULL CHECK (volume >= 0))WITH (timescaledb.hypertable,timescaledb.segmentby='ticker');This creates a
stock_pricestable with:- Partitioning by timestamp on column
ts. - Segmentation by
tickerfor optimal compression and query performance.
- Partitioning by timestamp on column
- Load the sample data
This approach writes data directly to the columnstore, bypassing the rowstore entirely. You get instant analytical performance.
From
psql:-- Enable direct to columnstore for this sessionSET timescaledb.enable_direct_compress_copy = on;-- Load data directly into columnstore\COPY stock_prices FROM 'sp500_stock_prices_3d_1s.csv' WITH (FORMAT csv, HEADER true);-- Verify data loadedSELECT COUNT(*) FROM stock_prices; - Run sample queries
Now explore the data with some analytical queries. Run these in your
psqlsession.Activate time measuring:
\timing onQuery 1: OHLCV per hour of AAPL.
Aggregate raw 1-second tick data into hourly candlesticks (open, high, low, close, volume) of the ticker
AAPL.SELECTtime_bucket('1 hour', ts) AS hour_bucket,ticker,FIRST(price, ts) AS open_price,MAX(price) AS high_price,MIN(price) AS low_price,LAST(price, ts) AS close_price,AVG(price) AS avg_price,SUM(volume) AS sum_volumeFROMstock_pricesWHEREticker = 'AAPL'GROUP BYhour_bucket,tickerORDER BYhour_bucket DESC;Query 2: trend analysis with a simple moving average (SMA) of MSFT.
Calculate a smoothing line to see trends over noise for the ticker
MSFTover 4 hours.WITH candles AS (SELECTtime_bucket('1 hour', ts) AS bucket,ticker,LAST(price, ts) AS close_priceFROM stock_pricesWHERE ticker = 'MSFT'GROUP BY bucket, ticker)SELECTbucket,ticker,close_price,AVG(close_price) OVER (PARTITION BY tickerORDER BY bucketROWS BETWEEN 3 PRECEDING AND CURRENT ROW) AS sma_4hoursFROM candlesORDER BY bucket DESC;Query 3: hour-over-hour return.
Compare the current price to the price exactly one hour ago to calculate percentage growth.
WITH hourly_close AS (SELECTtime_bucket('1 hour', ts) AS bucket,ticker,LAST(price, ts) AS closing_priceFROM stock_pricesGROUP BY bucket, ticker)SELECTbucket,ticker,closing_price,LAG(closing_price, 1) OVER (PARTITION BY ticker ORDER BY bucket) AS prev_close,((closing_price - LAG(closing_price, 1) OVER (PARTITION BY ticker ORDER BY bucket))/ LAG(closing_price, 1) OVER (PARTITION BY ticker ORDER BY bucket)) * 100 AS hourly_return_pctFROM hourly_close;Query 4: price volatility.
SELECTticker,AVG(price) AS avg_price,STDDEV(price) AS price_volatility,MAX(price) - MIN(price) AS price_spreadFROM stock_pricesWHERE ts > NOW() - INTERVAL '7 days'GROUP BY tickerHAVING count(*) > 10ORDER BY price_volatility DESC;
What's happening behind the scenes?
Section titled “What's happening behind the scenes?”Columnstore compression
Section titled “Columnstore compression”With tsdb.enable_columnstore=true:
- Data is stored in a hybrid row-columnar format.
- Analytical queries only scan the columns they need, for a large speedup.
- Typical compression ratios: 90% or more for time-series data.
- Compression happens transparently, with no changes to your queries.
Direct to columnstore
Section titled “Direct to columnstore”When you use SET timescaledb.enable_direct_compress_copy = on:
- Data loads directly into compressed columnstore format.
- Bypasses the rowstore entirely.
- Provides instant analytical performance, with no waiting for background compression.
Segmentation
Section titled “Segmentation”The tsdb.segmentby='ticker' setting:
- Groups data by ticker within each chunk.
- Improves compression ratios by keeping similar data together.
- Speeds up queries that filter by ticker.
- Is better for ticker-based analytics.
Continuous aggregates (Advanced)
Section titled “Continuous aggregates (Advanced)”For real-time dashboards, you can create continuous aggregates that automatically update:
-- Create a continuous aggregate for hourly candlesticksCREATE MATERIALIZED VIEW candlesticks_hourlyWITH (timescaledb.continuous) ASSELECT time_bucket('1 hour', ts) AS hour, ticker, FIRST(price, ts) AS open_price, MAX(price) AS high_price, MIN(price) AS low_price, LAST(price, ts) AS close_price, AVG(price) AS avg_price, SUM(volume) AS sum_volumeFROM stock_pricesGROUP BY hour, tickerORDER BY hour DESC, ticker ASC;
-- Add a refresh policy to keep it updatedSELECT add_continuous_aggregate_policy('candlesticks_hourly', start_offset => INTERVAL '2 hours', end_offset => INTERVAL '1 hour', schedule_interval => INTERVAL '1 hour');Now you can query candlesticks_hourly for instant results on pre-aggregated data:
SELECT * from candlesticks_hourly WHERE ticker = 'NFLX';