Composites#

This section contains 11 examples for Composites using the onetick-py.
Each example is a self-contained script that can be run against the OneTick Cloud sample databases.

# onetick-py WebAPI configuration for OneTick Cloud
import os
os.environ['OTP_WEBAPI'] = '1'
os.environ['OTP_HTTP_ADDRESS'] = 'https://rest.cloud.onetick.com'
os.environ['OTP_ACCESS_TOKEN_URL'] = 'https://cloud-auth.parent.onetick.com/realms/OMD/protocol/openid-connect/token'
os.environ['OTP_CLIENT_ID'] = '__FILL_IN__'
os.environ['OTP_CLIENT_SECRET'] = '__FILL_IN__'

Total Volume Per Venue Across Symbols#

Calculating the Volume Traded across all symbols for each venue within the Composite. e.g. US_COMP.
The DAY summary table includes the daily Volume rollup, making it unnecessary to calculate across the Trade table TRD.
The venue is stored in the field EXCHANGE for US_COMP, and QUOTE_VENUE for other composites.
The composite for all venues is typically added as empty quotes e.g. ''.
The primary venue is identified as PRIM.

import onetick.py as otp

# Retrieve DAY Records from US_COMP_DAILY
data = otp.DataSource(db='US_COMP_SAMPLE_DAILY', tick_type='DAY')

# Limit Fields
data = data[['EXCHANGE', 'VOLUME']]

# Merge across all symbols
data = otp.merge([data], symbols=otp.Symbols(db='US_COMP_SAMPLE_DAILY'), identify_input_ts=True)

# Aggregate: count number of quotes per venue
data = data.agg({'VOLUME': otp.agg.sum('VOLUME')}, group_by=['EXCHANGE'])

# Filter out the Composite
data = data.where(data['EXCHANGE'] != '')

# Filter out the Primary
data = data.where(data['EXCHANGE'] != 'PRIM')

# Run query across the time range.
result = otp.run(data,
                 start=otp.dt(2024, 1, 3),
                 end=otp.dt(2024, 1, 4),
                 timezone='America/New_York')
result
Time EXCHANGE VOLUME
0 2024-01-04 A 33890564
1 2024-01-04 B 45310224
2 2024-01-04 C 47420646
3 2024-01-04 D 5101343473
4 2024-01-04 H 220129350
... ... ... ...
14 2024-01-04 U 366155659
15 2024-01-04 V 234670455
16 2024-01-04 X 27713620
17 2024-01-04 Y 121288137
18 2024-01-04 Z 578921698

19 rows × 3 columns

Trade Count Per Venue#

Calculating the trade count for each venue within the Composite. e.g. CA_COMP.
The quotes are stored in the TRD table.
The venue is stored in the field TRADE_VENUE, except for US_COMP which uses EXCHANGE.

import onetick.py as otp

# Retrieve Trades from CA_COMP
data = otp.DataSource(db='CA_COMP_SAMPLE', tick_type='TRD')

# Aggregate: count number of trades per venue
data = data.agg({'TRADE_COUNT': otp.agg.count()}, group_by=['TRADE_VENUE'])

# Define Output fields
data = data[['TRADE_VENUE', 'TRADE_COUNT']]

# Run query for selected symbol (for TD) and time range.
result = otp.run(data,
                 start=otp.dt(2024, 1, 3, 9, 30),
                 end=otp.dt(2024, 1, 3, 16, 0),
                 timezone='America/Toronto',
                 symbols='TD')
result
Time TRADE_VENUE TRADE_COUNT
0 2024-01-03 16:00:00 CHIC 8746
1 2024-01-03 16:00:00 CSE2 18
2 2024-01-03 16:00:00 LYNX 93
3 2024-01-03 16:00:00 MATN 2879
4 2024-01-03 16:00:00 NEOD 18
... ... ... ...
10 2024-01-03 16:00:00 XATX 14
11 2024-01-03 16:00:00 XCX2 1296
12 2024-01-03 16:00:00 XCXD 1749
13 2024-01-03 16:00:00 XICX 13
14 2024-01-03 16:00:00 XTSE 21023

15 rows × 3 columns

Quote Count Per Venue#

Calculating the quote count for each venue within the Composite. e.g. CA_COMP.
The quotes are stored in the QTE table.
The venue is stored in the field QUOTE_VENUE, except for US_COMP which uses EXCHANGE.

import onetick.py as otp

# Retrieve Quotes from CA_COMP
data = otp.DataSource(db='CA_COMP_SAMPLE', tick_type='QTE')

# Aggregate: count number of quotes per venue
data = data.agg({'QUOTE_COUNT': otp.agg.count()}, group_by=['QUOTE_VENUE'])

# Define Output fields
data = data[['QUOTE_VENUE', 'QUOTE_COUNT']]

# Run query for selected symbol (for TD) and time range.
result = otp.run(data,
                 start=otp.dt(2024, 1, 3, 9, 30),
                 end=otp.dt(2024, 1, 3, 16, 0),
                 timezone='America/Toronto',
                 symbols='TD')
result
Time QUOTE_VENUE QUOTE_COUNT
0 2024-01-03 16:00:00 CHIC 350854
1 2024-01-03 16:00:00 CSE2 49538
2 2024-01-03 16:00:00 LYNX 59278
3 2024-01-03 16:00:00 NEOE 202624
4 2024-01-03 16:00:00 NEON 288203
5 2024-01-03 16:00:00 OMGA 168343
6 2024-01-03 16:00:00 PURE 81993
7 2024-01-03 16:00:00 XATS 144204
8 2024-01-03 16:00:00 XATX 856
9 2024-01-03 16:00:00 XCX2 246433
10 2024-01-03 16:00:00 XTSE 317337

EBBO Custom Calculation#

Calculate a Custom EBBO / NBBO based on Selected Venues using the European Composite Dataset EU_COMP.
The European Composite includes quotes from all European Venues, plus a consolidated NBBO.
A custom NBBO can be calculated by filtering consolidated quotes on venue using the QUOTE_VENUE field and currency using the CURRENCY field.
The virtual_ob() method is used in association with the ob_snapshot_wide() method to construct the new consolidated book.
Using parameter max_levels=1 to limit the resulting book to Top of Book (ToB) to produce the NBBO.

import onetick.py as otp

# Retrieve Quotes from EU_COMP
data = otp.DataSource(db='EU_COMP_SAMPLE', tick_type='QTE')

# Filter on Continuous Trading Periods
data = data.where(data['OMD_STATUS'] == 'T')

# Filter on Required Currency
data = data.where(data['CURRENCY'] == 'GBX')

# Filter on Selected Venues
data = data.where(data['QUOTE_VENUE'].isin('AQXE', 'BATE', 'CHIX', 'EQTC', 'TRQX', 'XLON'))

# Create a order book data format based on the quotes from each venue
data = data.virtual_ob(['QUOTE_VENUE'])

# Rebuild the NBBO from the order book data
data = data.ob_snapshot_wide(running=True, max_levels=1)

# Add Spread
data['SPREAD'] = data['ASK_PRICE'] - data['BID_PRICE']

# Filter out events that occur at the same timestamp, keeping the last event per timestamp
data['NEXT_TS'] = data['TIMESTAMP'][+1]
data = data.where(data['NEXT_TS'] > data['TIMESTAMP'])

# Define Output fields
data = data[['BID_PRICE', 'BID_SIZE', 'ASK_PRICE', 'ASK_SIZE', 'SPREAD']]

# Return first 100 Rows
data = data.limit(100)

# Run query for selected ISIN (for Vodafone) and time range.
result = otp.run(data,
                 start=otp.dt(2024, 1, 3, 8, 0),
                 end=otp.dt(2024, 1, 3, 16, 0),
                 timezone='Europe/London',
                 symbols='GB00BH4HKS39')
result
Time BID_PRICE BID_SIZE ASK_PRICE ASK_SIZE SPREAD
0 2024-01-03 08:00:00.072000 69.34 21964 70.27 757 0.93
1 2024-01-03 08:00:00.076000 69.34 44497 70.27 757 0.93
2 2024-01-03 08:00:01.024000 69.34 44300 70.27 757 0.93
3 2024-01-03 08:00:01.122000 69.34 43160 70.68 49218 1.34
4 2024-01-03 08:00:01.163000 69.34 43160 70.68 50617 1.34
... ... ... ... ... ... ...
95 2024-01-03 08:00:07.420198 70.06 3152 70.18 10252 0.12
96 2024-01-03 08:00:07.421000 70.07 2260 70.17 3461 0.10
97 2024-01-03 08:00:07.424000 70.08 100 70.17 3461 0.09
98 2024-01-03 08:00:07.427000 70.08 100 70.17 409 0.09
99 2024-01-03 08:00:07.435000 70.08 100 70.18 2300 0.10

100 rows × 6 columns

CBBO Custom Calculation#

Calculate a Custom CBBO / NBBO based on Selected Venues using the Canadian Composite Dataset CA_COMP.
The Canadian Composite includes quotes from all Canadian Venues, plus a consolidated NBBO.
A custom NBBO can be calculated by filtering on venue using the QUOTE_VENUE field.
The virtual_ob() method is used in association with the ob_snapshot_wide() method to construct the new consolidated book.
Using parameter max_levels=1 to limit the resulting book to Top of Book (ToB) to produce the NBBO.

import onetick.py as otp

# Retrieve Quotes from CA_COMP
data = otp.DataSource(db='CA_COMP_SAMPLE', tick_type='QTE')

# Filter on Continuous Trading Periods
data = data.where(data['OMD_STATUS'] == 'T')

# Filter on Selected Venues
data = data.where(
    data['QUOTE_VENUE'].isin('CHIC', 'CSE2', 'LYNX', 'NEOE', 'NEON', 'OMGA', 'PURE', 'XATS', 'XATX', 'XCX2', 'XTSE')
)

# Create a order book data format based on the quotes from each venue
data = data.virtual_ob(['QUOTE_VENUE'])

# Rebuild the NBBO from the order book data
data = data.ob_snapshot_wide(running=True, max_levels=1)

# Add Spread
data['SPREAD'] = data['ASK_PRICE'] - data['BID_PRICE']

# Filter out events that occur at the same timestamp, keeping the last event per timestamp
data['NEXT_TS'] = data['TIMESTAMP'][+1]
data = data.where(data['NEXT_TS'] > data['TIMESTAMP'])

# Define Output fields
data = data[['BID_PRICE', 'BID_SIZE', 'ASK_PRICE', 'ASK_SIZE', 'SPREAD']]

# Return first 100 Rows
data = data.limit(100)

# Run query for selected ISIN (for Vodafone) and time range.
result = otp.run(data,
                 start=otp.dt(2024, 1, 3, 9, 30),
                 end=otp.dt(2024, 1, 3, 16, 0),
                 timezone='America/Toronto',
                 symbols='TD')
result
Time BID_PRICE BID_SIZE ASK_PRICE ASK_SIZE SPREAD
0 2024-01-03 09:30:00.063 85.19 100 86.08 1100 0.89
1 2024-01-03 09:30:00.067 85.24 800 86.08 1100 0.84
2 2024-01-03 09:30:00.070 85.22 400 86.08 1100 0.86
3 2024-01-03 09:30:00.072 84.28 100 86.08 1100 1.80
4 2024-01-03 09:30:00.073 84.28 100 86.08 1100 1.80
... ... ... ... ... ... ...
95 2024-01-03 09:30:01.954 85.25 2700 85.31 1400 0.06
96 2024-01-03 09:30:01.963 85.25 2700 85.31 1300 0.06
97 2024-01-03 09:30:01.985 85.25 2700 85.31 1000 0.06
98 2024-01-03 09:30:01.986 85.25 3000 85.31 1000 0.06
99 2024-01-03 09:30:01.988 85.25 3600 85.31 1000 0.06

100 rows × 6 columns

Trade Volume to NBBO Calculation#

Calculate trade volume relative to NBBO levels for each trade.
Joins trades (TRD) with prevailing NBBO quotes and classifies each trade by NBBO level.
Classifies each trade volume by NBBO level (AT_MID, INSIDE_NBBO, AT_NBBO, OUTSIDE_NBBO).
Aggregates total volume by exchange and symbol for each NBBO classification.
Calculate trade volume relative to NBBO levels for each trade.

  • Step 1: For each trade, retrieve the prevailing NBBO at that moment

  • Step 2: Classify each trade volume by NBBO level (AT_MID, INSIDE_NBBO, AT_NBBO, OUTSIDE_NBBO)

  • Step 3: Aggregate total volume by exchange for each NBBO classification

import onetick.py as otp

# Retrieve trade ticks with PRICE and SIZE fields
trades = otp.DataSource(db='US_COMP', tick_type='TRD')
trades = trades[['PRICE', 'SIZE', 'EXCHANGE']]

# Retrieve prevailing NBBO quotes with BID_PRICE and ASK_PRICE fields
nbbo = otp.DataSource(db='US_COMP', tick_type='NBBO', back_to_first_tick=86400)
nbbo = nbbo[['BID_PRICE', 'ASK_PRICE']]

# Join trades with prevailing NBBO at trade time
joined = otp.join_by_time([trades, nbbo])

# Calculate mid price
joined['MID_PRICE'] = (joined['BID_PRICE'] + joined['ASK_PRICE']) / 2

# Classify trade volume by NBBO level using conditional logic (boolean * size = size or 0)
joined['VOLUME_AT_MID'] = (joined['PRICE'] == joined['MID_PRICE']) * joined['SIZE']

joined['VOLUME_INSIDE_NBBO'] = (
    (joined['PRICE'] > joined['BID_PRICE']) & (joined['PRICE'] < joined['ASK_PRICE'])
) * joined['SIZE']

joined['VOLUME_AT_NBBO'] = (
    (joined['PRICE'] == joined['BID_PRICE']) | (joined['PRICE'] == joined['ASK_PRICE'])
) * joined['SIZE']

joined['VOLUME_OUTSIDE_NBBO'] = (
    (joined['PRICE'] < joined['BID_PRICE']) | (joined['PRICE'] > joined['ASK_PRICE'])
) * joined['SIZE']

# Aggregate by exchange and symbol
data = joined.agg({
    'VOLUME_AT_MID': otp.agg.sum('VOLUME_AT_MID'),
    'VOLUME_INSIDE_NBBO': otp.agg.sum('VOLUME_INSIDE_NBBO'),
    'VOLUME_AT_NBBO': otp.agg.sum('VOLUME_AT_NBBO'),
    'VOLUME_OUTSIDE_NBBO': otp.agg.sum('VOLUME_OUTSIDE_NBBO'),
    'TRADE_COUNT': otp.agg.count()
}, group_by=['EXCHANGE'])

result = otp.run(
    data,
    symbols=['CSCO'],
    start=otp.dt(2024, 1, 3),
    end=otp.dt(2024, 1, 4),
    timezone='America/New_York'
)
result
Time EXCHANGE VOLUME_AT_MID VOLUME_INSIDE_NBBO VOLUME_AT_NBBO VOLUME_OUTSIDE_NBBO TRADE_COUNT
0 2024-01-04 A 7541 5108 14257 504 236
1 2024-01-04 B 2629 2569 30459 500 485
2 2024-01-04 C 7102 5942 107844 597 1832
3 2024-01-04 D 1551492 3702729 1936908 245550 37598
4 2024-01-04 H 71108 6834 228942 1319 2537
... ... ... ... ... ... ... ...
11 2024-01-04 U 143049 36803 484555 7797 5444
12 2024-01-04 V 162738 139259 259411 2846 4408
13 2024-01-04 X 7165 4350 39389 2120 604
14 2024-01-04 Y 12848 8303 144428 2580 1519
15 2024-01-04 Z 286876 110179 859658 13999 11603

16 rows × 7 columns

US Odd Lot NBBO Retrieval#

Retrieving the US NBBO including Odd Lots.
The US Composite provides data from the US Consolidated Tape, known as the SIP.
Historically Quotes and NBBO have been provided based on round lots, and Odd lot quotes have not been distributed.
Regulation has since been updated so that Odd lot quotes are additionally distributed, adding the Best Odd Lot Order (BOLO).
OneTick combines the BOLO with the NBBO to create NBBO_COMP, which is available from June 2026 onwards.

import onetick.py as otp

data = otp.DataSource(db='US_COMP', tick_type='NBBO_COMP')
data = data.limit(1000)
result = otp.run(data,
                 start=otp.dt(2026, 7, 23, 9, 30),
                 end=otp.dt(2026, 7, 23, 16, 0),
                 timezone='America/New_York',
                 symbols='CSCO')
result
Time BID_PRICE BID_SIZE BID_SIZE_TOTAL BID_EXCHANGE ASK_PRICE ASK_SIZE ASK_SIZE_TOTAL ASK_EXCHANGE IS_PRE_OPEN OMDSEQ
0 2026-07-23 09:30:00.005781958 111.60 2 2 P 111.97 1051 1051 Q 0 3
1 2026-07-23 09:30:00.021822151 111.60 10 12 K 111.97 1051 1051 Q 0 2
2 2026-07-23 09:30:00.026266020 111.60 93 105 Z 111.97 1051 1051 Q 0 2
3 2026-07-23 09:30:00.034203418 111.61 20 20 Q 111.97 1051 1051 Q 0 2
4 2026-07-23 09:30:00.039083007 111.63 1 1 K 111.97 1051 1051 Q 0 2
... ... ... ... ... ... ... ... ... ... ... ...
995 2026-07-23 09:30:05.063335703 112.18 1 2 K 112.18 3947 3947 Q 0 27
996 2026-07-23 09:30:05.063342433 112.18 1 2 K 112.18 100 100 Q 0 31
997 2026-07-23 09:30:05.063349178 112.18 1 2 K 112.28 20 20 P 0 37
998 2026-07-23 09:30:05.063371778 112.18 1 2 K 112.20 8 8 Q 0 47
999 2026-07-23 09:30:05.063379664 112.18 1 2 K 112.20 375 375 Q 0 53

1000 rows × 11 columns

US Odd Lot Quote Retrieval#

Retrieving the US Exchange Quotes including Odd Lots.
The US Composite provides data from the US Consolidated Tape, known as the SIP.
Historically Quotes and NBBO have been provided based on round lots, and Odd lot quotes have not been distributed.
Regulation has since been updated so that Odd lot quotes are additionally distributed, adding the Best Odd Lot Order (BOLO).
OneTick combines the BOLO with the exchange quotes to create QTE_COMP, which is available from June 2026 onwards.

import onetick.py as otp

data = otp.DataSource(db='US_COMP', tick_type='QTE_COMP')
data = data.limit(1000)
result = otp.run(data,
                 start=otp.dt(2026, 7, 23, 9, 30),
                 end=otp.dt(2026, 7, 23, 16, 0),
                 timezone='America/New_York',
                 symbols='CSCO')
result
Time EXCH_TIME SEQ_NUM EXCHANGE SOURCE BID_PRICE BID_SIZE ASK_PRICE ASK_SIZE QUOTE_COND RPI RESTRICTION_IND OMDSEQ
0 2026-07-23 09:30:00.005781958 2026-07-23 09:30:00.000189608 5529880 V N 105.97 100 117.57 100 R 1
1 2026-07-23 09:30:00.021822151 2026-07-23 09:30:00.021641304 5531561 K N 111.60 10 112.07 100 R 1
2 2026-07-23 09:30:00.026266020 2026-07-23 09:30:00.026085204 5531984 Z N 111.60 93 115.73 100 R 1
3 2026-07-23 09:30:00.027008979 2026-07-23 09:30:00.026811665 5532082 K N 111.60 10 112.07 100 R 1
4 2026-07-23 09:30:00.033916296 2026-07-23 09:30:00.033740082 5532589 Z N 111.60 93 115.73 100 R 1
... ... ... ... ... ... ... ... ... ... ... ... ... ...
995 2026-07-23 09:30:02.918799529 2026-07-23 09:30:02.918625018 5663344 J N 110.91 100 113.12 100 R 1
996 2026-07-23 09:30:02.918802170 2026-07-23 09:30:02.918628021 5663345 Y N 107.03 100 116.17 100 R 3
997 2026-07-23 09:30:02.920359376 2026-07-23 09:30:02.920017641 5663360 N N 110.53 100 117.56 200 R 1
998 2026-07-23 09:30:02.920741782 2026-07-23 09:30:02.920567335 5663365 Z N 111.98 200 112.46 148 R 3
999 2026-07-23 09:30:02.920749521 1969-12-31 19:00:00.000000000 5663366 K N 112.18 10 112.50 10 R 4

1000 rows × 13 columns

Joining NBBOs with and without Odd Lots#

Retrieving the US Exchange NBBO with and without Odd Lots (BOLO).
The NBBO with Odd Lots (NBBO_COMP) is joined to the NBBO without Odd Lots.
This returns the two sets of quotes: the wider spread without odd lots and the narrower spread with odd lots.

import onetick.py as otp

# NBBO without Odd Lots
nbbo = otp.DataSource(db='US_COMP', tick_type='NBBO')
nbbo = nbbo[['BID_PRICE', 'ASK_PRICE', 'BID_SIZE_TOTAL', 'ASK_SIZE_TOTAL']]

# NBBO with Odd Lots (BOLO) - renamed with a _COMP suffix
comp = otp.DataSource(db='US_COMP', tick_type='NBBO_COMP')
comp = comp[['BID_PRICE', 'ASK_PRICE', 'BID_SIZE_TOTAL', 'ASK_SIZE_TOTAL']]
comp = comp.rename(columns={'BID_PRICE': 'BID_PRICE_COMP',
                            'ASK_PRICE': 'ASK_PRICE_COMP',
                            'BID_SIZE_TOTAL': 'BID_SIZE_TOTAL_COMP',
                            'ASK_SIZE_TOTAL': 'ASK_SIZE_TOTAL_COMP'})

# As-of join: keep every NBBO tick, attaching the prevailing NBBO_COMP quote
data = otp.join_by_time([nbbo, comp])
data = data.limit(1000)
result = otp.run(data,
                 start=otp.dt(2026, 7, 23, 9, 30),
                 end=otp.dt(2026, 7, 23, 16, 0),
                 timezone='America/New_York',
                 symbols='CSCO')
result
Time BID_PRICE ASK_PRICE BID_SIZE_TOTAL ASK_SIZE_TOTAL BID_PRICE_COMP ASK_PRICE_COMP BID_SIZE_TOTAL_COMP ASK_SIZE_TOTAL_COMP
0 2026-07-23 09:30:00.005781958 111.55 111.97 100 1000 NaN NaN 0 0
1 2026-07-23 09:30:00.171091468 111.55 111.97 200 1000 111.81 111.97 20 1051
2 2026-07-23 09:30:00.189536667 111.55 111.97 300 1000 111.81 111.97 20 1051
3 2026-07-23 09:30:00.189544648 111.55 111.97 200 1000 111.81 111.97 20 1051
4 2026-07-23 09:30:00.348511535 111.55 112.00 200 100 111.81 111.97 20 1038
... ... ... ... ... ... ... ... ... ...
995 2026-07-23 09:30:17.288391537 112.33 112.44 100 100 112.34 112.36 5 20
996 2026-07-23 09:30:17.288406083 112.33 112.41 100 100 112.34 112.36 5 20
997 2026-07-23 09:30:17.288432221 112.33 112.40 100 100 112.33 112.36 100 20
998 2026-07-23 09:30:17.288450719 112.34 112.40 100 100 112.33 112.36 100 20
999 2026-07-23 09:30:17.288464242 112.34 112.40 200 100 112.34 112.36 100 20

1000 rows × 9 columns

Joining NBBOs with and without Odd Lots and Calculating Skews and Liquidity#

Retrieving the US Exchange NBBO with and without Odd Lots, calculating Skews and Liquidity.
The NBBO with Odd Lots (NBBO_COMP) is joined to the NBBO without Odd Lots, returning the two sets of quotes: the wider spread without odd lots and the narrower spread with odd lots.
These are compared to produce the Bid and Ask Skews (BID_SKEW, ASK_SKEW) and Percentage Skew (PCNT_BID_SKEW, PCNT_ASK_SKEW).
Additional liquidity visible in the odd lot quote is also calculated by comparing the Bid and Ask Sizes between the two sets of quotes.
This is not the full additional liquidity, as there may be price levels between the two sets of quotes.

import onetick.py as otp

# NBBO without Odd Lots
nbbo = otp.DataSource(db='US_COMP', tick_type='NBBO')
nbbo = nbbo[['BID_PRICE', 'ASK_PRICE', 'BID_SIZE_TOTAL', 'ASK_SIZE_TOTAL']]

# NBBO with Odd Lots (BOLO) - renamed with a _COMP suffix
comp = otp.DataSource(db='US_COMP', tick_type='NBBO_COMP')
comp = comp[['BID_PRICE', 'ASK_PRICE', 'BID_SIZE_TOTAL', 'ASK_SIZE_TOTAL']]
comp = comp.rename(columns={'BID_PRICE': 'BID_PRICE_COMP',
                            'ASK_PRICE': 'ASK_PRICE_COMP',
                            'BID_SIZE_TOTAL': 'BID_SIZE_TOTAL_COMP',
                            'ASK_SIZE_TOTAL': 'ASK_SIZE_TOTAL_COMP'})

# As-of join: keep every NBBO tick, attaching the prevailing NBBO_COMP quote
data = otp.join_by_time([nbbo, comp])

# Skews - odd lot book is narrower, so compare against the round lot NBBO
data['BID_SKEW'] = data['BID_PRICE_COMP'] - data['BID_PRICE']
data['ASK_SKEW'] = data['ASK_PRICE'] - data['ASK_PRICE_COMP']
data['PCNT_BID_SKEW'] = 100 * (data['BID_PRICE_COMP'] - data['BID_PRICE']) / data['BID_PRICE']
data['PCNT_ASK_SKEW'] = 100 * (data['ASK_PRICE'] - data['ASK_PRICE_COMP']) / data['ASK_PRICE']

# Additional visible liquidity from the odd lot quote.
# If the price levels match, the addition is the size difference, otherwise it is the full odd lot size.
data['BID_SIZE_ADDITION'] = data.apply(
    lambda r: r['BID_SIZE_TOTAL_COMP'] - r['BID_SIZE_TOTAL']
    if r['BID_PRICE_COMP'] == r['BID_PRICE']
    else r['BID_SIZE_TOTAL_COMP']
)
data['ASK_SIZE_ADDITION'] = data.apply(
    lambda r: r['ASK_SIZE_TOTAL_COMP'] - r['ASK_SIZE_TOTAL']
    if r['ASK_PRICE_COMP'] == r['ASK_PRICE']
    else r['ASK_SIZE_TOTAL_COMP']
)

data = data.limit(1000)
result = otp.run(data,
                 start=otp.dt(2026, 7, 23, 9, 30),
                 end=otp.dt(2026, 7, 23, 16, 0),
                 timezone='America/New_York',
                 symbols='CSCO')
result
Time BID_PRICE ASK_PRICE BID_SIZE_TOTAL ASK_SIZE_TOTAL BID_PRICE_COMP ASK_PRICE_COMP BID_SIZE_TOTAL_COMP ASK_SIZE_TOTAL_COMP BID_SKEW ASK_SKEW PCNT_BID_SKEW PCNT_ASK_SKEW BID_SIZE_ADDITION ASK_SIZE_ADDITION
0 2026-07-23 09:30:00.005781958 111.55 111.97 100 1000 NaN NaN 0 0 NaN NaN NaN NaN 0 0
1 2026-07-23 09:30:00.171091468 111.55 111.97 200 1000 111.81 111.97 20 1051 0.26 0.00 0.233079 0.000000 20 51
2 2026-07-23 09:30:00.189536667 111.55 111.97 300 1000 111.81 111.97 20 1051 0.26 0.00 0.233079 0.000000 20 51
3 2026-07-23 09:30:00.189544648 111.55 111.97 200 1000 111.81 111.97 20 1051 0.26 0.00 0.233079 0.000000 20 51
4 2026-07-23 09:30:00.348511535 111.55 112.00 200 100 111.81 111.97 20 1038 0.26 0.03 0.233079 0.026786 20 1038
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
995 2026-07-23 09:30:17.288391537 112.33 112.44 100 100 112.34 112.36 5 20 0.01 0.08 0.008902 0.071149 5 20
996 2026-07-23 09:30:17.288406083 112.33 112.41 100 100 112.34 112.36 5 20 0.01 0.05 0.008902 0.044480 5 20
997 2026-07-23 09:30:17.288432221 112.33 112.40 100 100 112.33 112.36 100 20 0.00 0.04 0.000000 0.035587 0 20
998 2026-07-23 09:30:17.288450719 112.34 112.40 100 100 112.33 112.36 100 20 -0.01 0.04 -0.008902 0.035587 100 20
999 2026-07-23 09:30:17.288464242 112.34 112.40 200 100 112.34 112.36 100 20 0.00 0.04 0.000000 0.035587 -100 20

1000 rows × 15 columns

TWAP Bars for Spreads, Skews and Added Liquidity Comparing NBBOs with and without Odd Lots#

Calculating 1 Minute TWAP Bars for Spreads, Skews and added liquidity comparing NBBOs with and without Odd Lots.
The NBBO with Odd Lots (NBBO_COMP) is joined to the NBBO without Odd Lots.
Spreads, Skews and added visible Liquidity are calculated.
These are then calculated as 1 minute Time weighted averages using tw_average() across the period.

import onetick.py as otp

# NBBO without Odd Lots
nbbo = otp.DataSource(db='US_COMP', tick_type='NBBO')
nbbo = nbbo[['BID_PRICE', 'ASK_PRICE', 'BID_SIZE_TOTAL', 'ASK_SIZE_TOTAL']]

# NBBO with Odd Lots (BOLO) - renamed with a _COMP suffix
comp = otp.DataSource(db='US_COMP', tick_type='NBBO_COMP')
comp = comp[['BID_PRICE', 'ASK_PRICE', 'BID_SIZE_TOTAL', 'ASK_SIZE_TOTAL']]
comp = comp.rename(columns={'BID_PRICE': 'BID_PRICE_COMP',
                            'ASK_PRICE': 'ASK_PRICE_COMP',
                            'BID_SIZE_TOTAL': 'BID_SIZE_TOTAL_COMP',
                            'ASK_SIZE_TOTAL': 'ASK_SIZE_TOTAL_COMP'})

# As-of join: keep every NBBO tick, attaching the prevailing NBBO_COMP quote
data = otp.join_by_time([nbbo, comp])

# Spreads with and without odd lots
data['SPREAD'] = data['ASK_PRICE'] - data['BID_PRICE']
data['SPREAD_COMP'] = data['ASK_PRICE_COMP'] - data['BID_PRICE_COMP']

# Percentage skews
data['PCNT_BID_SKEW'] = 100 * (data['BID_PRICE_COMP'] - data['BID_PRICE']) / data['BID_PRICE']
data['PCNT_ASK_SKEW'] = 100 * (data['ASK_PRICE'] - data['ASK_PRICE_COMP']) / data['ASK_PRICE']

# Additional visible liquidity from the odd lot quote
data['BID_SIZE_ADDITION'] = data.apply(
    lambda r: r['BID_SIZE_TOTAL_COMP'] - r['BID_SIZE_TOTAL']
    if r['BID_PRICE_COMP'] == r['BID_PRICE']
    else r['BID_SIZE_TOTAL_COMP']
)
data['ASK_SIZE_ADDITION'] = data.apply(
    lambda r: r['ASK_SIZE_TOTAL_COMP'] - r['ASK_SIZE_TOTAL']
    if r['ASK_PRICE_COMP'] == r['ASK_PRICE']
    else r['ASK_SIZE_TOTAL_COMP']
)

# 1 minute time weighted averages
data = data.agg({'TWA_SPREAD': otp.agg.tw_average('SPREAD'),
                 'TWA_SPREAD_COMP': otp.agg.tw_average('SPREAD_COMP'),
                 'TWA_PCNT_BID_SKEW': otp.agg.tw_average('PCNT_BID_SKEW'),
                 'TWA_PCNT_ASK_SKEW': otp.agg.tw_average('PCNT_ASK_SKEW'),
                 'TWA_BID_SIZE_ADDITION': otp.agg.tw_average('BID_SIZE_ADDITION'),
                 'TWA_ASK_SIZE_ADDITION': otp.agg.tw_average('ASK_SIZE_ADDITION')},
                bucket_interval=otp.Minute(1))

result = otp.run(data,
                 start=otp.dt(2026, 7, 23, 9, 30),
                 end=otp.dt(2026, 7, 23, 16, 0),
                 timezone='America/New_York',
                 symbols='CSCO')
result
Time TWA_SPREAD TWA_SPREAD_COMP TWA_PCNT_BID_SKEW TWA_PCNT_ASK_SKEW TWA_BID_SIZE_ADDITION TWA_ASK_SIZE_ADDITION
0 2026-07-23 09:31:00 0.215450 0.140410 0.024416 0.041910 53.203005 19.718561
1 2026-07-23 09:32:00 0.123505 0.080868 0.015353 0.022436 28.342016 13.206969
2 2026-07-23 09:33:00 0.095515 0.060749 0.005024 0.025806 44.442403 25.211660
3 2026-07-23 09:34:00 0.103634 0.085108 0.006949 0.009550 18.700700 25.212788
4 2026-07-23 09:35:00 0.092061 0.079801 0.004404 0.006504 -0.621712 70.537566
... ... ... ... ... ... ... ...
385 2026-07-23 15:56:00 0.019129 0.015048 0.001639 0.001984 92.985752 71.156811
386 2026-07-23 15:57:00 0.013511 0.011700 0.000818 0.000788 102.146870 96.556970
387 2026-07-23 15:58:00 0.015162 0.012494 0.001250 0.001115 118.091640 93.751892
388 2026-07-23 15:59:00 0.014644 0.012188 0.000438 0.001737 103.466443 62.913740
389 2026-07-23 16:00:00 0.012814 0.011879 0.000243 0.000586 57.165562 53.505681

390 rows × 7 columns