Volt Active Data
← Back to All Guides

VWAP Demo — Quick Start Guide

Volume Weighted Average Price · Real-Time Financial Analytics
1
Architecture
2
Schema (DDL)
3
Stored Procedure
4
VoltSP Pipeline
5
See It Working
Step 1 of 5
1
Architecture Overview
VWAP (Volume Weighted Average Price) is a critical financial benchmark calculated as the cumulative sum of (price × volume) divided by cumulative volume over a trading session. Unlike a simple average, VWAP weights each trade by its size, giving institutional traders a fair-value reference for execution quality. This demo streams real S&P 500 ticker data through Kafka into VoltSP, which calls a VoltDB stored procedure to compute OHLCV candlesticks with session VWAP across 1-minute, 15-minute, and 60-minute time windows — all at sub-10ms latency and 300K+ trades per second.

Prerequisites

VoltDB
14.1+
VoltSP
1.7+
Apache Kafka
3.6+ (KRaft)
Java
21+
Maven
3.8+
Understanding the Data Flow
VWAP (Volume Weighted Average Price) is calculated by summing the product of price and volume, then dividing by total volume.
  • Kafka streams raw trade events in real-time
  • VoltDB ingests via VoltSP pipeline
  • A stored procedure computes VWAP per symbol
  • Results update continuously with <10ms latency
VoltDB handles 300K+ trades/second on production while maintaining ACID guarantees.
2
Schema Definition (DDL)
Create the OHLCV Table
First, we create the stocktick_vmap table to store OHLCV (Open, High, Low, Close, Volume) candlestick data with VWAP.
  • symbol is the stock ticker (e.g., AAPL)
  • tickdate stores the time window start
  • timescale tracks granularity (1min, 15min, 60min)
  • OHLC prices track price movement within window
The composite PRIMARY KEY (symbol, tickdate, timescale) enables multiple time granularities per symbol.
Create the VWAP State Table
This table maintains session-based running totals needed to calculate VWAP for each symbol.
  • Tracks cumulative value × volume for VWAP calculation
  • cumulative_volume stores running sum of volumes
  • session_start anchors calculations to trading session
  • VWAP = cumulative_value_volume / cumulative_volume
Session-based VWAP resets each trading day, matching real market conventions.
Create the Tick Stream
A stream captures all incoming ticks for export to downstream systems.
  • STREAM is a special table type for high-velocity writes
  • PARTITION ON COLUMN enables parallel ingestion
  • EXPORT TO TARGET sends data to external systems
  • Used for alerts, analytics, or audit trails
Streams provide fire-and-forget writes with guaranteed delivery to export targets.
Register the Stored Procedure
Register the Java stored procedure with partitioning information.
  • PARTITION ON TABLE specifies the partition column
  • FROM CLASS loads the compiled Java class
  • Single-partition procedures run without coordination
  • Enables millions of transactions per second
The procedure is partitioned on symbol, so all trades for the same stock are processed by the same core.
SQL
-- Create dummy table for time window calculations
CREATE TABLE dummy (x VARCHAR(1) NOT NULL, PRIMARY KEY (x));

-- Create the OHLCV table with VWAP
CREATE TABLE stocktick_vmap
(symbol varchar(10) not null
,tickdate timestamp not null
,timescale varchar(10) not null
,open_price decimal not null
,high_price decimal not null
,low_price decimal not null
,close_price decimal not null
,volume decimal not null
,total_value decimal not null
,vmap decimal
,primary key (symbol,tickdate,timescale));

PARTITION TABLE stocktick_vmap ON COLUMN symbol;

-- Session VWAP state table
CREATE TABLE vwap_state (
    symbol VARCHAR(10) NOT NULL,
    session_start TIMESTAMP NOT NULL,
    cumulative_value_volume DECIMAL,
    cumulative_volume DECIMAL,
    last_update TIMESTAMP,
    PRIMARY KEY (symbol, session_start)
);

PARTITION TABLE vwap_state ON COLUMN symbol;

-- Create stream for tick data export
CREATE STREAM ALL_TICKS
  PARTITION ON COLUMN SYMBOL
  EXPORT TO TARGET ALERTS (
    Symbol varchar(10) NOT NULL,
    Tickdate timestamp NOT NULL,
    Value decimal NOT NULL,
    Volume decimal NOT NULL
);

-- Summary view for monitoring
CREATE VIEW TICKS_SUMMARY (
  Symbol, Ticks_count
) AS SELECT Symbol, count(*)
   FROM ALL_TICKS GROUP BY symbol;

-- Register the stored procedure
CREATE PROCEDURE
   PARTITION ON TABLE stocktick_vmap COLUMN symbol
   FROM CLASS com.voltactivedata.voltdb.storedProcedures
              .ReportTickSessionAnchor;

-- Initialize dummy table (required for TIME_WINDOW calculations)
INSERT INTO dummy VALUES ('X');
3
Java Stored Procedure
Package & Imports
Define the package and import necessary VoltDB classes.
  • Package matches your project structure
  • Import VoltDB core classes (SQLStmt, VoltProcedure, VoltTable)
  • BigDecimal for precise financial calculations
  • TimestampType for VoltDB timestamps
VoltDB procedures must extend VoltProcedure base class.
Constants & SQL Builders
Define constants for time windows and SQL statement templates.
  • MINUTE, HOUR, MINUTES_15 define time granularities
  • STOCKTICK_COLUMNS lists the table columns
  • INSERT_TICK_START/END build parameterized INSERT statements
  • String concatenation creates complete SQL at compile time
Using constants makes the code more maintainable and less error-prone.
Query Statements
SQL statements to query existing candlesticks for each time window.
  • getRollingMinute and getSessionTime compute time boundaries
  • getMinute, get15Minutes, getHour query existing candles
  • TIME_WINDOW() function buckets timestamps
  • SQLStmt objects are pre-compiled at load time
Pre-compiled statements eliminate SQL parsing overhead at runtime.
Insert & Update Statements
SQL statements to insert new candlesticks and update existing ones.
  • insertTickMinute/15Minutes/Hour create new records
  • updateTick modifies existing candlestick data
  • Uses parameterized statements for security
  • Arrays theGets/theInserts enable iteration over time windows
Separating insert and update enables efficient upsert pattern.
VWAP Calculation Helper
Helper method to calculate VWAP from cumulative totals.
  • Divides cumulative value×volume by cumulative volume
  • Uses RoundingMode.HALF_UP for standard rounding
  • Returns 6 decimal places for precision
  • Handles division by zero case
VWAP = Σ(price × volume) / Σ(volume)
Session VWAP State Manager
Method to fetch, update, and persist session VWAP state.
  • Fetches existing cumulative totals from vwap_state table
  • Checks if same trading session (resets at market open)
  • Accumulates new trade contribution
  • Queues UPSERT to persist updated state
Session VWAP resets at market open (9:30 AM = 570 minutes from midnight).
Run Method - Query Phase
The entry point queues all necessary queries and executes them.
  • Queues SELECT_VWAP_STATE for current state
  • Queues getHour/get15Minutes/getMinute for existing candles
  • Queues getRollingMinute and getSessionTime for time boundaries
  • Single voltExecuteSQL() batch executes all queries
Batching queries minimizes round-trips and maximizes throughput.
Run Method - Update Phase
Process results and update/insert OHLCV candlesticks with session VWAP.
  • Iterates through all time windows (1-min, 15-min, 60-min)
  • Inserts new candle if none exists for time window
  • Updates low/high/close/volume if candle exists
  • Uses session VWAP for all time window aggregations
Each candle stores the session VWAP calculated from all trades that day.
Export & Commit
Export the tick to the stream and commit the transaction.
  • Queue the stream insert to ALL_TICKS
  • voltExecuteSQL(true) commits transaction
  • All changes are atomic (all or nothing)
  • Returns results to caller
The "true" parameter commits the transaction - essential for durability.
Java
/*
 * Copyright (C) 2025 Volt Active Data Inc.
 */
package com.voltactivedata.voltdb.storedProcedures;

import java.math.RoundingMode;
import java.math.BigDecimal;
import org.voltdb.SQLStmt;
import org.voltdb.VoltProcedure;
import org.voltdb.VoltTable;
import org.voltdb.VoltTableRow;
import org.voltdb.types.TimestampType;

public class ReportTickSessionAnchor extends VoltProcedure {

    private static final String STOCKTICK_COLUMNS = "(symbol, tickdate, timescale, open_price"
            + ", high_price, low_price, close_price, volume, total_value, vmap) ";
    private static final String MINUTE = "MINUTES_1";
    private static final String HOUR = "MINUTES_60";
    private static final String MINUTES_15 = "MINUTES_15";
    private static final String INSERT_TICK_START = "INSERT INTO stocktick_vmap " + STOCKTICK_COLUMNS
            + "VALUES (?,TIME_WINDOW(";

    private static final String INSERT_TICK_END = ",?,?,?,?,?,?,?,?);";
    private static final String MINUTE_INSERT = "MINUTE,1,?)";
    private static final String MINUTES_15_INSERT = "MINUTE,15,?)";
    private static final String HOUR_INSERT = "MINUTE,60,?)";

    public static final SQLStmt getRollingMinute = new SQLStmt(
            "SELECT TIME_WINDOW(MINUTE,1,?) TICKDATE FROM dummy WHERE x = 'X';");

    public static final SQLStmt getSessionTime = new SQLStmt(
            "SELECT DATEADD(MINUTE,570,TIME_WINDOW(DAY, 1, ?)) tickdate FROM dummy WHERE x = 'X';");

    public static final SQLStmt get15Minutes = new SQLStmt(
            "SELECT * FROM stocktick_vmap WHERE symbol = ? AND tickdate = TIME_WINDOW(MINUTE,15,?) AND timescale = '"+MINUTES_15+"';");

    public static final SQLStmt getMinute = new SQLStmt(
            "SELECT * FROM stocktick_vmap WHERE symbol = ? AND tickdate = TIME_WINDOW(MINUTE,1,?) AND timescale = '"+MINUTE+"';");

    public static final SQLStmt getHour = new SQLStmt(
            "SELECT * FROM stocktick_vmap WHERE symbol = ? AND tickdate = TIME_WINDOW(MINUTE,60,?) AND timescale = '"+HOUR+"';");

    public static final SQLStmt insertTickMinute = new SQLStmt(
            INSERT_TICK_START+MINUTE_INSERT+INSERT_TICK_END);

    public static final SQLStmt insertTick15Minutes = new SQLStmt(
            INSERT_TICK_START+MINUTES_15_INSERT+INSERT_TICK_END);

    public static final SQLStmt insertTickHour = new SQLStmt(
            INSERT_TICK_START+HOUR_INSERT+INSERT_TICK_END);

    public static final SQLStmt updateTick = new SQLStmt(
            "UPDATE stocktick_vmap "
                    + "SET low_price = ?, high_price = ?, close_price = ?, volume = ?, total_value = ?, vmap = ? "
                    + "WHERE symbol = ? AND tickdate = ? AND timescale = ? ;");

    public final SQLStmt SELECT_VWAP_STATE = new SQLStmt(
            "SELECT symbol, session_start, cumulative_value_volume, cumulative_volume " +
                    "FROM vwap_state WHERE symbol = ? and session_start = (SELECT DATEADD(MINUTE,570,TIME_WINDOW(DAY, 1, ?)) tickdate FROM dummy WHERE x = 'X');"
    );

    public final SQLStmt UPSERT_VWAP_STATE = new SQLStmt(
            "UPSERT INTO vwap_state (symbol, session_start, cumulative_value_volume, " +
                    "cumulative_volume, last_update) " +
                    "VALUES (?, ?, ?, ?, ?);"
    );

    public final SQLStmt INSERT_ALLTICKS_STREAM = new SQLStmt("INSERT INTO ALL_TICKS (Symbol, Tickdate, Value, Volume) VALUES (?, ?, ?, ?);");

    final String[] theTimes = {HOUR, MINUTES_15, MINUTE};
    final SQLStmt[] theGets = {getHour, get15Minutes, getMinute,};
    final SQLStmt[] theInserts = {insertTickHour, insertTick15Minutes, insertTickMinute};

    private BigDecimal calculateVWAP(BigDecimal cumulativeValueVolume, BigDecimal cumulativeVolume) {
        if (cumulativeVolume.compareTo(BigDecimal.ZERO) == 0) {
            return BigDecimal.ZERO;
        }
        return cumulativeValueVolume.divide(cumulativeVolume, 6, RoundingMode.HALF_UP);
    }

    private BigDecimal getAndUpdateSessionVWAP(TimestampType sessionTime, VoltTable[] vwap_state, String symbol, TimestampType tickdate, BigDecimal value, BigDecimal volume) {
        TimestampType currentSessionStart = sessionTime;
        VoltTable[] results = vwap_state;

        BigDecimal sessionCumulativeValueVolume = BigDecimal.ZERO;
        BigDecimal sessionCumulativeVolume = BigDecimal.ZERO;
        TimestampType storedSessionStart = null;

        if (results[0].getRowCount() > 0) {
            VoltTableRow row = results[0].fetchRow(0);
            storedSessionStart = row.getTimestampAsTimestamp(1);

            if (storedSessionStart.equals(currentSessionStart)) {
                sessionCumulativeValueVolume = row.getDecimalAsBigDecimal(2);
                sessionCumulativeVolume = row.getDecimalAsBigDecimal(3);
            }
        }

        BigDecimal tradeValueVolume = value.multiply(volume);
        sessionCumulativeValueVolume = sessionCumulativeValueVolume.add(tradeValueVolume);
        sessionCumulativeVolume = sessionCumulativeVolume.add(volume);

        BigDecimal sessionVWAP = calculateVWAP(sessionCumulativeValueVolume, sessionCumulativeVolume);

        voltQueueSQL(UPSERT_VWAP_STATE, symbol, currentSessionStart,
                sessionCumulativeValueVolume, sessionCumulativeVolume, tickdate);

        return sessionVWAP;
    }

    public VoltTable[] run(String symbol, TimestampType tickdate, BigDecimal value, BigDecimal volume)
            throws VoltAbortException {

        voltQueueSQL(SELECT_VWAP_STATE, symbol, tickdate);
        for (int i = 0; i < theTimes.length; i++) {
            voltQueueSQL(theGets[i], symbol, tickdate);
        }
        voltQueueSQL(getRollingMinute, tickdate);
        voltQueueSQL(getSessionTime, tickdate);

        VoltTable[] currentTicks = voltExecuteSQL();

        currentTicks[currentTicks.length - 2].advanceRow();
        final TimestampType rollingTickdate = currentTicks[currentTicks.length - 2].getTimestampAsTimestamp("TICKDATE");

        currentTicks[currentTicks.length - 1].advanceRow();
        final TimestampType sessionTime = currentTicks[currentTicks.length - 1].getTimestampAsTimestamp("TICKDATE");

        BigDecimal sessionVWAP = getAndUpdateSessionVWAP(sessionTime, currentTicks, symbol, tickdate, value, volume);

        for (int i = 1; i < theTimes.length + 1; i++) {
            if (!currentTicks[i].advanceRow()) {
                BigDecimal vmap = sessionVWAP;
                voltQueueSQL(theInserts[i - 1], symbol, tickdate, theTimes[i - 1], value, value, value, value, volume,
                        value.multiply(volume), vmap);
            } else {
                final TimestampType actualTickdate = currentTicks[i].getTimestampAsTimestamp("TICKDATE");
                BigDecimal low = currentTicks[i].getDecimalAsBigDecimal("LOW_PRICE");
                if (value.compareTo(low) == -1) {
                    low = value;
                }
                BigDecimal high = currentTicks[i].getDecimalAsBigDecimal("HIGH_PRICE");
                if (value.compareTo(high) == 1) {
                    high = value;
                }
                final BigDecimal newEntryVolume = volume.add(currentTicks[i].getDecimalAsBigDecimal("VOLUME"));
                final BigDecimal extraTotalValue = value.multiply(volume);
                final BigDecimal newEntryTotalValue = extraTotalValue
                        .add(currentTicks[i].getDecimalAsBigDecimal("TOTAL_VALUE"));
                BigDecimal newVmap = sessionVWAP;
                voltQueueSQL(updateTick, low, high, value, newEntryVolume, newEntryTotalValue, newVmap, symbol,
                        actualTickdate, theTimes[i - 1]);
            }
        }
        voltQueueSQL(INSERT_ALLTICKS_STREAM, symbol, tickdate, value, volume);

        return voltExecuteSQL(true);
    }
}
4
VoltSP Pipeline
Pipeline Configuration (YAML)
Configure the VoltSP streaming pipeline in config.yaml with resources, source, and sink.
  • version: 1 declares the config format version
  • resources define VoltDB client connection(s)
  • source configures Kafka consumer settings
  • Runs standalone alongside the VoltDB cluster
VoltSP uses YAML configuration - no Java code needed for simple pipelines.
JavaScript Processor
Transform Kafka messages to VoltDB procedure calls using embedded JavaScript.
  • processors section defines transformation logic
  • JavaScript runs in GraalVM for high performance
  • Access Java types via Java.type()
  • Return VoltProcedureRequest with procedure name and params
JavaScript processors avoid compilation - just edit the YAML and restart.
Sink Configuration
Configure the VoltDB sink to send transformed messages to the stored procedure.
  • sink.voltdb-procedure sends to VoltDB
  • voltClientResource references the named resource
  • Procedure name comes from VoltProcedureRequest
  • Connection pooling and retries handled automatically
Start VoltSP with: ./voltsp -l license.xml --metrics-port 11799 --parallelism=1 config.yaml
YAML
# config.yaml - VoltSP Pipeline Configuration
version: 1
name: "kafka-to-voltdb-with-js"

resources:
  - name: voltdb
    voltdb-client:
      servers:
        - localhost:21212

source:
  kafka:
    bootstrapServers:
      - "localhost:9092"
    topicNames: "ticker-data"
    groupId: "vwap-consumer"
    startingOffset: "EARLIEST"
    keyDeserializer: "org.apache.kafka.common.serialization.StringDeserializer"
    valueDeserializer: "com.voltactivedata.vwapdemo.kafka.TickerDataDeserializer"

processors:
  - javascript:
      script: |
        function process(input) {
          var tickerData = input.getValue();

          // Import Java types
          var VoltProcedureRequest = Java.type(
            'org.voltdb.stream.plugin.volt.api.VoltProcedureRequest');
          var VoltTimestamp = Java.type('org.voltdb.types.TimestampType');
          var BigDecimal = Java.type('java.math.BigDecimal');

          // Build procedure call
          return new VoltProcedureRequest(
            "ReportTickSessionAnchor",
            [
              tickerData.getSymbol(),
              new VoltTimestamp(+tickerData.getEventDate()),
              new BigDecimal(tickerData.getPrice()),
              new BigDecimal(tickerData.getVolume())
            ]
          );
        }

sink:
  voltdb-procedure:
    voltClientResource: "voltdb"

# Run with:
# ./voltsp -l ~/license.xml --metrics-port 11799 --parallelism=1 config.yaml
5
See It Working
Live OHLCV + VWAP Candlesticks
Watch real-time OHLCV candlestick updates with session VWAP as trades stream in from Kafka.
  • Each row shows symbol, timescale, OHLC prices, volume, and session VWAP
  • Multiple time granularities: 1-min, 15-min, 60-min
  • Session VWAP resets daily at market open (9:30 AM)
  • Query stocktick_vmap for candlesticks, vwap_state for session totals
Try: SELECT symbol, timescale, close_price, vmap FROM stocktick_vmap WHERE timescale = 'MINUTES_1' ORDER BY tickdate DESC;
SQL
-- View latest candlesticks with VWAP
SELECT symbol, timescale, tickdate, open_price, high_price,
       low_price, close_price, volume, vmap AS session_vwap
FROM stocktick_vmap
WHERE timescale = 'MINUTES_1'
ORDER BY tickdate DESC
LIMIT 20;

-- View session VWAP state
SELECT symbol, session_start, cumulative_value_volume,
       cumulative_volume, last_update
FROM vwap_state
ORDER BY last_update DESC;

-- Check tick throughput
SELECT Symbol, Ticks_count
FROM TICKS_SUMMARY
ORDER BY Ticks_count DESC;