Technical Analysis#

This section contains 34 examples for Technical Analysis 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__'

Aggressor Volume Imbalance#

Aggressor Volume Imbalance from Trade Data.
Calculated for venues that publish AGGRESSOR_SIDE.
BUY_VOLUME is the SIZE where AGGRESSOR_SIDE = ‘B’, SELL_VOLUME where AGGRESSOR_SIDE = ‘S’.
Volume is aggregated into 1-minute buckets.

import onetick.py as otp

# Retrieve trade data for VOD (LSE publishes AGGRESSOR_SIDE)
data = otp.DataSource(db='LSE', tick_type='TRD')
data = data[['SIZE', 'AGGRESSOR_SIDE']]

# Split each trade's size into buy and sell volume based on the aggressor side
data['BUY_VOLUME'] = data.apply(lambda r: r['SIZE'] if r['AGGRESSOR_SIDE'] == 'B' else 0)
data['SELL_VOLUME'] = data.apply(lambda r: r['SIZE'] if r['AGGRESSOR_SIDE'] == 'S' else 0)

# Sum buy, sell and total volume per 1-minute bucket
data = data.agg(
    {
        'VOLUME': otp.agg.sum('SIZE'),
        'BUY_VOLUME': otp.agg.sum('BUY_VOLUME'),
        'SELL_VOLUME': otp.agg.sum('SELL_VOLUME'),
    },
    bucket_interval=otp.Minute(1)
)

# Absolute and percentage imbalance
data['IMBALANCE_VOLUME'] = data['BUY_VOLUME'] - data['SELL_VOLUME']
data['PCNT_IMBALANCE_VOLUME'] = (
    100 * (data['BUY_VOLUME'] - data['SELL_VOLUME']) /
    (data['BUY_VOLUME'] + data['SELL_VOLUME'])
)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 8),
    end=otp.dt(2024, 1, 3, 16),
    timezone='Europe/London',
    symbols='VOD'
)

result
Time VOLUME BUY_VOLUME SELL_VOLUME IMBALANCE_VOLUME PCNT_IMBALANCE_VOLUME
0 2024-01-03 08:01:00 319591 24426 29489 -5063 -9.390708
1 2024-01-03 08:02:00 170283 125338 24915 100423 66.835937
2 2024-01-03 08:03:00 40964 5459 26581 -21122 -65.923845
3 2024-01-03 08:04:00 59600 0 24974 -24974 -100.000000
4 2024-01-03 08:05:00 120771 40161 21311 18850 30.664368
... ... ... ... ... ... ...
475 2024-01-03 15:56:00 72860 39746 14682 25064 46.049827
476 2024-01-03 15:57:00 45149 16032 26057 -10025 -23.818575
477 2024-01-03 15:58:00 75597 11840 59452 -47612 -66.784492
478 2024-01-03 15:59:00 27675 7752 4716 3036 24.350337
479 2024-01-03 16:00:00 26166 0 21029 -21029 -100.000000

480 rows × 6 columns

Average True Range (ATR)#

Average True Range (ATR) Indicator from Trade Data.
The three candidate ranges use 1-minute HIGH, LOW and the prevailing price at the start of the window (PRICE_N_BACK):

HL   = HIGH - LOW
H_PC = abs(HIGH - PRICE_N_BACK)
L_PC = abs(LOW - PRICE_N_BACK)

The True Range (TR) is the maximum of the three, and ATR is the 14-minute moving average of TR.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Rolling 1-minute high, low and prevailing price at the start of the window
data = data.agg(
    {
        'HIGH': otp.agg.max('PRICE'),
        'LOW': otp.agg.min('PRICE'),
        'PRICE_N_BACK': otp.agg.first('PRICE'),
    },
    running=True,
    bucket_interval=otp.Minute(1),
    all_fields=True
)

# The three candidate ranges (H_PC and L_PC are absolute values)
data['HL'] = data['HIGH'] - data['LOW']
data['H_PC'] = data.apply(lambda r: r['HIGH'] - r['PRICE_N_BACK']
                          if r['HIGH'] - r['PRICE_N_BACK'] >= 0
                          else r['PRICE_N_BACK'] - r['HIGH'])
data['L_PC'] = data.apply(lambda r: r['LOW'] - r['PRICE_N_BACK']
                          if r['LOW'] - r['PRICE_N_BACK'] >= 0
                          else r['PRICE_N_BACK'] - r['LOW'])

# True Range is the maximum of the three candidate ranges
def true_range(r):
    if r['HL'] >= r['H_PC'] and r['HL'] >= r['L_PC']:
        return r['HL']
    if r['H_PC'] >= r['L_PC']:
        return r['H_PC']
    return r['L_PC']

data['TR'] = data.apply(true_range)

# ATR: 14-minute moving average of the True Range
data = data.agg(
    {'ATR': otp.agg.average('TR')},
    running=True,
    bucket_interval=otp.Minute(14),
    all_fields=True
)

data = data[['PRICE', 'HIGH', 'LOW', 'PRICE_N_BACK', 'TR', 'ATR']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE HIGH LOW PRICE_N_BACK TR ATR
0 2024-01-03 09:30:00.065443591 50.02 50.02 50.020 50.02 0.000 0.000000
1 2024-01-03 09:30:00.111130049 50.16 50.16 50.020 50.02 0.140 0.070000
2 2024-01-03 09:30:00.127459523 50.17 50.17 50.020 50.02 0.150 0.096667
3 2024-01-03 09:30:00.128498068 50.17 50.17 50.020 50.02 0.150 0.110000
4 2024-01-03 09:30:00.135190071 50.13 50.17 50.020 50.02 0.150 0.118000
... ... ... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 50.23 50.002 50.02 0.228 0.195606
996 2024-01-03 09:30:07.378683771 50.12 50.23 50.002 50.02 0.228 0.195639
997 2024-01-03 09:30:07.379088757 50.12 50.23 50.002 50.02 0.228 0.195671
998 2024-01-03 09:30:07.390014965 50.12 50.23 50.002 50.02 0.228 0.195704
999 2024-01-03 09:30:07.396341167 50.09 50.23 50.002 50.02 0.228 0.195736

1000 rows × 7 columns

Average True Range (ATR) on 1 Min Bars#

Average True Range (ATR) Indicator from 1 Minute Trade Bars.
Returns the Average True Range (ATR) indicator computed on pre-built 1-minute bars.
The three candidate ranges use the bar HIGH, LOW and the previous bar’s LAST (PRIOR_LAST):

HL   = HIGH - LOW
H_PC = abs(HIGH - PRIOR_LAST)
L_PC = abs(LOW - PRIOR_LAST)

The True Range (TR) is the maximum of the three, and ATR is the 14-minute moving average of TR.

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['HIGH', 'LOW', 'LAST']]

# Previous bar's last price (LAST[-1] is the previous bar's LAST)
data['PRIOR_LAST'] = data['LAST'][-1]

# The three candidate ranges (H_PC and L_PC are absolute values)
data['HL'] = data['HIGH'] - data['LOW']
data['H_PC'] = data.apply(lambda r: r['HIGH'] - r['PRIOR_LAST']
                          if r['HIGH'] - r['PRIOR_LAST'] >= 0
                          else r['PRIOR_LAST'] - r['HIGH'])
data['L_PC'] = data.apply(lambda r: r['LOW'] - r['PRIOR_LAST']
                          if r['LOW'] - r['PRIOR_LAST'] >= 0
                          else r['PRIOR_LAST'] - r['LOW'])

# True Range is the maximum of the three candidate ranges
def true_range(r):
    if r['HL'] >= r['H_PC'] and r['HL'] >= r['L_PC']:
        return r['HL']
    if r['H_PC'] >= r['L_PC']:
        return r['H_PC']
    return r['L_PC']

data['TR'] = data.apply(true_range)

# ATR: 14-minute moving average of the True Range
data = data.agg(
    {'ATR': otp.agg.average('TR')},
    running=True,
    bucket_interval=otp.Minute(14),
    all_fields=True
)

data = data[['LAST', 'HIGH', 'LOW', 'PRIOR_LAST', 'TR', 'ATR']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST HIGH LOW PRIOR_LAST TR ATR
0 2024-01-03 09:31:00 50.160 50.2200 50.0020 NaN 0.2180 0.218000
1 2024-01-03 09:32:00 50.090 50.1750 50.0800 50.160 0.0950 0.156500
2 2024-01-03 09:33:00 50.040 50.0900 50.0301 50.090 0.0599 0.124300
3 2024-01-03 09:34:00 50.025 50.0600 50.0000 50.040 0.0600 0.108225
4 2024-01-03 09:35:00 50.020 50.0400 50.0000 50.025 0.0400 0.094580
... ... ... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 50.5100 50.4700 50.495 0.0400 0.043114
385 2024-01-03 15:56:00 50.505 50.5199 50.4700 50.475 0.0499 0.045607
386 2024-01-03 15:57:00 50.515 50.5300 50.4850 50.505 0.0450 0.048107
387 2024-01-03 15:58:00 50.520 50.5450 50.5100 50.515 0.0350 0.045964
388 2024-01-03 15:59:00 50.530 50.5450 50.5100 50.520 0.0350 0.046321

389 rows × 7 columns

Bollinger Bands#

Bollinger Bands from Trade Data.
A rolling average and rolling standard deviation are calculated across trade PRICE.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Rolling 5-minute moving average and standard deviation of PRICE
data = data.agg(
    {
        'MVG_AVG_PRICE': otp.agg.average('PRICE'),
        'MVG_STDDEV_PRICE': otp.agg.stddev('PRICE'),
    },
    running=True,
    bucket_interval=otp.Minute(5),
    all_fields=True
)

# Upper and Lower Bollinger Bands (2 standard deviations from the moving average)
data['BOLLINGER_UPPER_BAND'] = data['MVG_AVG_PRICE'] + 2 * data['MVG_STDDEV_PRICE']
data['BOLLINGER_LOWER_BAND'] = data['MVG_AVG_PRICE'] - 2 * data['MVG_STDDEV_PRICE']

data = data[['PRICE', 'SIZE', 'MVG_AVG_PRICE', 'BOLLINGER_UPPER_BAND', 'BOLLINGER_LOWER_BAND']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE SIZE MVG_AVG_PRICE BOLLINGER_UPPER_BAND BOLLINGER_LOWER_BAND
0 2024-01-03 09:30:00.065443591 50.02 2 50.020000 50.020000 50.020000
1 2024-01-03 09:30:00.111130049 50.16 3 50.090000 50.230000 49.950000
2 2024-01-03 09:30:00.127459523 50.17 100 50.116667 50.253618 49.979716
3 2024-01-03 09:30:00.128498068 50.17 5 50.130000 50.257279 50.002721
4 2024-01-03 09:30:00.135190071 50.13 46 50.130000 50.243842 50.016158
... ... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 2 50.132580 50.231373 50.033787
996 2024-01-03 09:30:07.378683771 50.12 50 50.132567 50.231314 50.033821
997 2024-01-03 09:30:07.379088757 50.12 100 50.132555 50.231255 50.033854
998 2024-01-03 09:30:07.390014965 50.12 100 50.132542 50.231196 50.033888
999 2024-01-03 09:30:07.396341167 50.09 1 50.132499 50.231141 50.033858

1000 rows × 6 columns

Bollinger Bandwidth#

Bollinger Bandwidth from Trade Data.
A rolling average and rolling standard deviation are calculated across trade PRICE.
The moving window is defined as 5 minutes.
The Upper and Lower Bollinger Bands are calculated at 2 standard deviations from the average.
BOLLINGER_BANDWIDTH is 4 times the MVG_STDDEV_PRICE (the full band width).
PCNT_BOLLINGER_BANDWIDTH expresses the bandwidth as a percentage of the moving average.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Rolling 5-minute moving average and standard deviation of PRICE
data = data.agg(
    {
        'MVG_AVG_PRICE': otp.agg.average('PRICE'),
        'MVG_STDDEV_PRICE': otp.agg.stddev('PRICE'),
    },
    running=True,
    bucket_interval=otp.Minute(5),
    all_fields=True
)

# Bollinger Bands, Bandwidth, and Bandwidth as a percentage of the moving average
data['BOLLINGER_UPPER_BAND'] = data['MVG_AVG_PRICE'] + 2 * data['MVG_STDDEV_PRICE']
data['BOLLINGER_LOWER_BAND'] = data['MVG_AVG_PRICE'] - 2 * data['MVG_STDDEV_PRICE']
data['BOLLINGER_BANDWIDTH'] = 4 * data['MVG_STDDEV_PRICE']
data['PCNT_BOLLINGER_BANDWIDTH'] = 100 * (4 * data['MVG_STDDEV_PRICE'] / data['MVG_AVG_PRICE'])

data = data[['PRICE', 'SIZE', 'MVG_AVG_PRICE', 'BOLLINGER_UPPER_BAND', 'BOLLINGER_LOWER_BAND',
             'BOLLINGER_BANDWIDTH', 'PCNT_BOLLINGER_BANDWIDTH']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE SIZE MVG_AVG_PRICE BOLLINGER_UPPER_BAND BOLLINGER_LOWER_BAND BOLLINGER_BANDWIDTH PCNT_BOLLINGER_BANDWIDTH
0 2024-01-03 09:30:00.065443591 50.02 2 50.020000 50.020000 50.020000 0.000000 0.000000
1 2024-01-03 09:30:00.111130049 50.16 3 50.090000 50.230000 49.950000 0.280000 0.558994
2 2024-01-03 09:30:00.127459523 50.17 100 50.116667 50.253618 49.979716 0.273902 0.546528
3 2024-01-03 09:30:00.128498068 50.17 5 50.130000 50.257279 50.002721 0.254558 0.507797
4 2024-01-03 09:30:00.135190071 50.13 46 50.130000 50.243842 50.016158 0.227684 0.454187
... ... ... ... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 2 50.132580 50.231373 50.033787 0.197586 0.394126
996 2024-01-03 09:30:07.378683771 50.12 50 50.132567 50.231314 50.033821 0.197493 0.393942
997 2024-01-03 09:30:07.379088757 50.12 100 50.132555 50.231255 50.033854 0.197400 0.393757
998 2024-01-03 09:30:07.390014965 50.12 100 50.132542 50.231196 50.033888 0.197308 0.393573
999 2024-01-03 09:30:07.396341167 50.09 1 50.132499 50.231141 50.033858 0.197283 0.393523

1000 rows × 8 columns

Donchian Channels#

Donchian Channels from Trade Data.
The most common period is 20 (here, 20 minutes).
UPPER_CHANNEL is the rolling maximum price over the last 20 minutes.
LOWER_CHANNEL is the rolling minimum price over the last 20 minutes.
MID_CHANNEL is the midpoint between the upper and lower channels.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Rolling 20-minute maximum (upper) and minimum (lower) channels
data = data.agg(
    {
        'UPPER_CHANNEL': otp.agg.max('PRICE'),
        'LOWER_CHANNEL': otp.agg.min('PRICE'),
    },
    running=True,
    bucket_interval=otp.Minute(20),
    all_fields=True
)

# Middle channel: midpoint of the upper and lower channels
data['MID_CHANNEL'] = (data['UPPER_CHANNEL'] + data['LOWER_CHANNEL']) / 2

data = data[['PRICE', 'UPPER_CHANNEL', 'LOWER_CHANNEL', 'MID_CHANNEL']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE UPPER_CHANNEL LOWER_CHANNEL MID_CHANNEL
0 2024-01-03 09:30:00.065443591 50.02 50.02 50.020 50.020
1 2024-01-03 09:30:00.111130049 50.16 50.16 50.020 50.090
2 2024-01-03 09:30:00.127459523 50.17 50.17 50.020 50.095
3 2024-01-03 09:30:00.128498068 50.17 50.17 50.020 50.095
4 2024-01-03 09:30:00.135190071 50.13 50.17 50.020 50.095
... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 50.23 50.002 50.116
996 2024-01-03 09:30:07.378683771 50.12 50.23 50.002 50.116
997 2024-01-03 09:30:07.379088757 50.12 50.23 50.002 50.116
998 2024-01-03 09:30:07.390014965 50.12 50.23 50.002 50.116
999 2024-01-03 09:30:07.396341167 50.09 50.23 50.002 50.116

1000 rows × 5 columns

Donchian Channels on 1 Min Bars#

Donchian Channels from 1 Minute Trade Bars.
The most common period is 20 (here, 20 one-minute bars).
UPPER_CHANNEL is the rolling maximum of the bar HIGH over the last 20 bars.
LOWER_CHANNEL is the rolling minimum of the bar LOW over the last 20 bars.
MID_CHANNEL is the midpoint between the upper and lower channels.

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['HIGH', 'LOW', 'LAST']]

# Rolling 20-bar maximum HIGH (upper) and minimum LOW (lower) channels
data = data.agg(
    {
        'UPPER_CHANNEL': otp.agg.max('HIGH'),
        'LOWER_CHANNEL': otp.agg.min('LOW'),
    },
    running=True,
    bucket_interval=20,
    bucket_units='ticks',
    all_fields=True
)

# Middle channel: midpoint of the upper and lower channels
data['MID_CHANNEL'] = (data['UPPER_CHANNEL'] + data['LOWER_CHANNEL']) / 2

data = data[['LAST', 'UPPER_CHANNEL', 'LOWER_CHANNEL', 'MID_CHANNEL']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST UPPER_CHANNEL LOWER_CHANNEL MID_CHANNEL
0 2024-01-03 09:31:00 50.160 50.22 50.002 50.111
1 2024-01-03 09:32:00 50.090 50.22 50.002 50.111
2 2024-01-03 09:33:00 50.040 50.22 50.002 50.111
3 2024-01-03 09:34:00 50.025 50.22 50.000 50.110
4 2024-01-03 09:35:00 50.020 50.22 50.000 50.110
... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 50.68 50.470 50.575
385 2024-01-03 15:56:00 50.505 50.68 50.470 50.575
386 2024-01-03 15:57:00 50.515 50.68 50.470 50.575
387 2024-01-03 15:58:00 50.520 50.68 50.470 50.575
388 2024-01-03 15:59:00 50.530 50.68 50.470 50.575

389 rows × 5 columns

Liquidity Comparison#

Liquidity Comparison - Bid-Ask Spread Analysis with Order Book Summary.
This example analyzes bid-ask VWAP spreads from order book summary.
Calculates spread in absolute terms and in basis points (bps) relative to mid-price.

import onetick.py as otp

# Get order book summary data from OTQ_CHAIN
# OB_SUMMARY provides best bid/ask prices from order book snapshots
data = otp.DataSource(db='LSE_SAMPLE', tick_type='PRL_FULL')
data = data.ob_summary(bucket_interval=60, max_depth_shares=1000)
data = data[['BID_VWAP', 'ASK_VWAP']]

# Calculate spread metrics

data['SPREAD'] = data['ASK_VWAP'] - data['BID_VWAP']
data['MID'] = (data['ASK_VWAP'] + data['BID_VWAP']) / 2
data['SPREAD_BPS'] = 10000 * data['SPREAD'] / data['MID']

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3),
    end=otp.dt(2024, 1, 4),
    timezone='UTC',
    symbols='HSBA'
)

result
Time BID_VWAP ASK_VWAP SPREAD MID SPREAD_BPS
0 2024-01-03 00:01:00 590.55 640.0 49.45 615.275 803.70566
1 2024-01-03 00:02:00 590.55 640.0 49.45 615.275 803.70566
2 2024-01-03 00:03:00 590.55 640.0 49.45 615.275 803.70566
3 2024-01-03 00:04:00 590.55 640.0 49.45 615.275 803.70566
4 2024-01-03 00:05:00 590.55 640.0 49.45 615.275 803.70566
... ... ... ... ... ... ...
1435 2024-01-03 23:56:00 590.55 640.0 49.45 615.275 803.70566
1436 2024-01-03 23:57:00 590.55 640.0 49.45 615.275 803.70566
1437 2024-01-03 23:58:00 590.55 640.0 49.45 615.275 803.70566
1438 2024-01-03 23:59:00 590.55 640.0 49.45 615.275 803.70566
1439 2024-01-04 00:00:00 590.55 640.0 49.45 615.275 803.70566

1440 rows × 6 columns

Market Breadth TRIN Snapshot#

Market Breadth - TRIN Snapshot (Daily).
TRIN < 1.0 = volume favoring advances (bullish).
TRIN > 1.0 = volume favoring declines (bearish).
TRIN = 1.0 = neutral.

This snapshot calculates TRIN for the entire trading day by comparing current price to day’s open price for each symbol, weighted by total volume.

import onetick.py as otp

# Get trade data for multiple symbols
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Aggregate per symbol: get open, close, and total volume for the day
daily_stats = data.agg({
    'OPEN_PRICE': otp.agg.first('PRICE'),
    'CURRENT_PRICE': otp.agg.last('PRICE'),
    'TOTAL_VOLUME': otp.agg.sum('SIZE')
})

# Classify each symbol as advancing or declining
daily_stats['IS_ADVANCING'] = (daily_stats['CURRENT_PRICE'] > daily_stats['OPEN_PRICE']).astype(int)
daily_stats['IS_DECLINING'] = (daily_stats['CURRENT_PRICE'] < daily_stats['OPEN_PRICE']).astype(int)
daily_stats['ADVANCING_VOLUME'] = daily_stats['TOTAL_VOLUME'] * (daily_stats['CURRENT_PRICE'] > daily_stats['OPEN_PRICE']).astype(int)
daily_stats['DECLINING_VOLUME'] = daily_stats['TOTAL_VOLUME'] * (daily_stats['CURRENT_PRICE'] < daily_stats['OPEN_PRICE']).astype(int)

# Merge per-symbol stats across all symbols for TRIN calculation
daily_stats = otp.merge([daily_stats], separate_db_name=True, identify_input_ts=True,
                        symbols=['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META', 'JPM'])

# Aggregate across all symbols to calculate TRIN
trin_result = daily_stats.agg({
    'ADVANCING_SYMBOLS': otp.agg.sum('IS_ADVANCING'),
    'declining_symbols': otp.agg.sum('IS_DECLINING'),
    'ADVANCING_VOLUME': otp.agg.sum('ADVANCING_VOLUME'),
    'DECLINING_VOLUME': otp.agg.sum('DECLINING_VOLUME')
})

# Calculate TRIN: (advancing symbols / declining symbols) / (advancing volume / declining volume)
trin_result['trin'] = (
    (trin_result['ADVANCING_SYMBOLS'] / trin_result['declining_symbols']) /
    (trin_result['ADVANCING_VOLUME'] / trin_result['DECLINING_VOLUME'])
)

result = otp.run(
    trin_result,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York'
)
result
Time ADVANCING_SYMBOLS declining_symbols ADVANCING_VOLUME DECLINING_VOLUME trin
0 2024-01-03 16:30:00 3 5 86246054 266398168 1.853289

Market Breadth TRIN Time-Series#

Market Breadth - TRIN Time-Series (5-Minute Candles).
TRIN < 1.0 = volume favoring advances (bullish).
TRIN > 1.0 = volume favoring declines (bearish).

This time-series tracks how market breadth evolves throughout the day for each 5-minute candle, symbols are classified as advancing or declining based on comparison of their close to the previous candle’s close.

import onetick.py as otp

# Get trade data for multiple symbols
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Create 5-minute candles: get close and volume per symbol per candle
candles = data.agg({
    'CLOSE_PRICE': otp.agg.last('PRICE'),
    'TOTAL_VOLUME': otp.agg.sum('SIZE')
}, bucket_interval=300)

# Calculate previous candle's close for each symbol
candles['LAST_CLOSE'] = candles['CLOSE_PRICE'][-1]

# Filter to only candles with a previous close
candles = candles.dropna()

# Classify each symbol as advancing or declining
candles['IS_ADVANCING'] = (candles['CLOSE_PRICE'] > candles['LAST_CLOSE']).astype(int)
candles['IS_DECLINING'] = (candles['CLOSE_PRICE'] < candles['LAST_CLOSE']).astype(int)
candles['ADVANCING_VOLUME'] = candles['TOTAL_VOLUME'] * (candles['CLOSE_PRICE'] > candles['LAST_CLOSE']).astype(int)
candles['DECLINING_VOLUME'] = candles['TOTAL_VOLUME'] * (candles['CLOSE_PRICE'] < candles['LAST_CLOSE']).astype(int)

# Merge candles across all symbols for TRIN calculation
candles = otp.merge([candles], separate_db_name=True, identify_input_ts=True,
                    symbols=['AAPL', 'MSFT', 'GOOGL', 'TSLA', 'AMZN', 'NVDA', 'META', 'JPM'])

# Aggregate across all symbols per timestamp to calculate TRIN time-series
trin_timeseries = candles.agg({
    'ADVANCING_SYMBOLS': otp.agg.sum('IS_ADVANCING'),
    'DECLINING_SYMBOLS': otp.agg.sum('IS_DECLINING'),
    'ADVANCING_VOLUME': otp.agg.sum('ADVANCING_VOLUME'),
    'DECLINING_VOLUME': otp.agg.sum('DECLINING_VOLUME')
}, bucket_interval=300)

# Calculate TRIN: (advancing symbols / declining symbols) / (advancing volume / declining volume)
trin_timeseries['trin'] = (trin_timeseries['ADVANCING_SYMBOLS'] / trin_timeseries['DECLINING_SYMBOLS']) / \
                          (trin_timeseries['ADVANCING_VOLUME'] / trin_timeseries['DECLINING_VOLUME'])

result = otp.run(
    trin_timeseries,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York'
)

result
Time ADVANCING_SYMBOLS DECLINING_SYMBOLS ADVANCING_VOLUME DECLINING_VOLUME trin
0 2024-01-03 09:35:00 0 0 0 0 NaN
1 2024-01-03 09:40:00 3 5 5234260 4223485 0.484135
2 2024-01-03 09:45:00 6 2 3250791 4637772 4.279979
3 2024-01-03 09:50:00 3 5 2505300 4913899 1.176841
4 2024-01-03 09:55:00 1 7 312205 5940112 2.718046
... ... ... ... ... ... ...
79 2024-01-03 16:10:00 4 4 1107741 505165 0.456032
80 2024-01-03 16:15:00 4 2 1535165 62848 0.081878
81 2024-01-03 16:20:00 3 5 16311 33108 1.217878
82 2024-01-03 16:25:00 5 3 105467 6075 0.096002
83 2024-01-03 16:30:00 3 5 1243 29102 14.047627

84 rows × 6 columns

Maximum Drawdown (MDD)#

Maximum Drawdown (MDD %) from Trade Data.
The running high price is tracked from the start of the period.
PCNT_DRAWDOWN is the percentage difference between the current price and the running high:

PCNT_DRAWDOWN = 100 * (PRICE - RUNNING_HIGH_PRICE) / RUNNING_HIGH_PRICE

MAX_PCNT_DRAWDOWN is the running minimum of PCNT_DRAWDOWN (the largest drawdown so far).

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Running high price from the start of the period (cumulative running max)
data = data.agg(
    {'RUNNING_HIGH_PRICE': otp.agg.max('PRICE')},
    running=True,
    all_fields=True
)

# Percentage drawdown from the running high
data['PCNT_DRAWDOWN'] = 100 * (data['PRICE'] - data['RUNNING_HIGH_PRICE']) / data['RUNNING_HIGH_PRICE']

# Maximum drawdown: running minimum of the percentage drawdown
data = data.agg(
    {'MAX_PCNT_DRAWDOWN': otp.agg.min('PCNT_DRAWDOWN')},
    running=True,
    all_fields=True
)

data = data[['PRICE', 'RUNNING_HIGH_PRICE', 'PCNT_DRAWDOWN', 'MAX_PCNT_DRAWDOWN']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE RUNNING_HIGH_PRICE PCNT_DRAWDOWN MAX_PCNT_DRAWDOWN
0 2024-01-03 09:30:00.065443591 50.02 50.02 0.000000 0.000000
1 2024-01-03 09:30:00.111130049 50.16 50.16 0.000000 0.000000
2 2024-01-03 09:30:00.127459523 50.17 50.17 0.000000 0.000000
3 2024-01-03 09:30:00.128498068 50.17 50.17 0.000000 0.000000
4 2024-01-03 09:30:00.135190071 50.13 50.17 -0.079729 -0.079729
... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 50.23 -0.218993 -0.334861
996 2024-01-03 09:30:07.378683771 50.12 50.23 -0.218993 -0.334861
997 2024-01-03 09:30:07.379088757 50.12 50.23 -0.218993 -0.334861
998 2024-01-03 09:30:07.390014965 50.12 50.23 -0.218993 -0.334861
999 2024-01-03 09:30:07.396341167 50.09 50.23 -0.278718 -0.334861

1000 rows × 5 columns

Maximum Drawdown (MDD) on 1 Min Bars#

Maximum Drawdown (MDD %) from 1 Minute Trade Bars.
The running high price is tracked from the start of the period using the bar HIGH.
PCNT_DRAWDOWN is the percentage difference between the current bar LAST and the running high:

PCNT_DRAWDOWN = 100 * (LAST - RUNNING_HIGH_PRICE) / RUNNING_HIGH_PRICE

MAX_PCNT_DRAWDOWN is the running minimum of PCNT_DRAWDOWN (the largest drawdown so far).

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['HIGH', 'LAST']]

# Running high price from the start of the period (cumulative running max of the bar HIGH)
data = data.agg(
    {'RUNNING_HIGH_PRICE': otp.agg.max('HIGH')},
    running=True,
    all_fields=True
)

# Percentage drawdown from the running high
data['PCNT_DRAWDOWN'] = 100 * (data['LAST'] - data['RUNNING_HIGH_PRICE']) / data['RUNNING_HIGH_PRICE']

# Maximum drawdown: running minimum of the percentage drawdown
data = data.agg(
    {'MAX_PCNT_DRAWDOWN': otp.agg.min('PCNT_DRAWDOWN')},
    running=True,
    all_fields=True
)

data = data[['LAST', 'RUNNING_HIGH_PRICE', 'PCNT_DRAWDOWN', 'MAX_PCNT_DRAWDOWN']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST RUNNING_HIGH_PRICE PCNT_DRAWDOWN MAX_PCNT_DRAWDOWN
0 2024-01-03 09:31:00 50.160 50.22 -0.119474 -0.119474
1 2024-01-03 09:32:00 50.090 50.22 -0.258861 -0.258861
2 2024-01-03 09:33:00 50.040 50.22 -0.358423 -0.358423
3 2024-01-03 09:34:00 50.025 50.22 -0.388292 -0.388292
4 2024-01-03 09:35:00 50.020 50.22 -0.398248 -0.398248
... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 50.68 -0.404499 -0.556439
385 2024-01-03 15:56:00 50.505 50.68 -0.345304 -0.556439
386 2024-01-03 15:57:00 50.515 50.68 -0.325572 -0.556439
387 2024-01-03 15:58:00 50.520 50.68 -0.315706 -0.556439
388 2024-01-03 15:59:00 50.530 50.68 -0.295975 -0.556439

389 rows × 5 columns

On-Balance Volume (OBV)#

On-Balance Volume (OBV) from Trade Data.
Retrieves the PRICE, SIZE and PRIOR_PRICE (the previous tick’s price, PRICE[-1]).

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Previous price (PRICE[-1] is the previous tick's price)
data['PRIOR_PRICE'] = data['PRICE'][-1]

# Signed volume based on price direction
def signed_size(r):
    if r['PRICE'] > r['PRIOR_PRICE']:
        return r['SIZE']
    if r['PRICE'] < r['PRIOR_PRICE']:
        return -r['SIZE']
    return 0

data['SIGNED_SIZE'] = data.apply(signed_size)

# On-Balance Volume: cumulative sum of signed volume
# running=True with no bucket_interval accumulates from query start to each tick
data = data.agg(
    {'OBV': otp.agg.sum('SIGNED_SIZE')},
    running=True,
    all_fields=True
)

data = data[['PRICE', 'SIZE', 'SIGNED_SIZE', 'OBV']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE SIZE SIGNED_SIZE OBV
0 2024-01-03 09:30:00.065443591 50.02 2 2 2
1 2024-01-03 09:30:00.111130049 50.16 3 3 5
2 2024-01-03 09:30:00.127459523 50.17 100 100 105
3 2024-01-03 09:30:00.128498068 50.17 5 0 105
4 2024-01-03 09:30:00.135190071 50.13 46 -46 59
... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 2 -2 261522
996 2024-01-03 09:30:07.378683771 50.12 50 0 261522
997 2024-01-03 09:30:07.379088757 50.12 100 0 261522
998 2024-01-03 09:30:07.390014965 50.12 100 0 261522
999 2024-01-03 09:30:07.396341167 50.09 1 -1 261521

1000 rows × 5 columns

On-Balance Volume (OBV) on 1 Min Bars#

On-Balance Volume (OBV) from 1 Minute Trade Bars.
Retrieves the bar LAST, VOLUME and the previous bar’s LAST (PRIOR_LAST, LAST[-1]).

SIGNED_VOLUME is +VOLUME when LAST > PRIOR_LAST, -VOLUME when LAST < PRIOR_LAST, else 0.

OBV is the cumulative sum of SIGNED_VOLUME across the bars.

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST', 'VOLUME']]

# Previous bar's last price (LAST[-1] is the previous bar's LAST)
data['PRIOR_LAST'] = data['LAST'][-1]

# Signed volume based on the bar-over-bar price direction
def signed_volume(r):
    if r['LAST'] > r['PRIOR_LAST']:
        return r['VOLUME']
    if r['LAST'] < r['PRIOR_LAST']:
        return -r['VOLUME']
    return 0

data['SIGNED_VOLUME'] = data.apply(signed_volume)

# On-Balance Volume: cumulative sum of signed volume
# running=True with no bucket_interval accumulates from query start to each bar
data = data.agg(
    {'OBV': otp.agg.sum('SIGNED_VOLUME')},
    running=True,
    all_fields=True
)

data = data[['LAST', 'VOLUME', 'SIGNED_VOLUME', 'OBV']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST VOLUME SIGNED_VOLUME OBV
0 2024-01-03 09:31:00 50.160 69657 69657 69657
1 2024-01-03 09:32:00 50.090 39792 -39792 29865
2 2024-01-03 09:33:00 50.040 26904 -26904 2961
3 2024-01-03 09:34:00 50.025 32673 -32673 -29712
4 2024-01-03 09:35:00 50.020 32783 -32783 -62495
... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 169383 -169383 872242
385 2024-01-03 15:56:00 50.505 185335 185335 1057577
386 2024-01-03 15:57:00 50.515 166268 166268 1223845
387 2024-01-03 15:58:00 50.520 346983 346983 1570828
388 2024-01-03 15:59:00 50.530 166368 166368 1737196

389 rows × 5 columns

Order Flow Imbalance (OFI)#

Order Flow Imbalance (OFI) from Quote Data.
Order Flow Imbalance (OFI) measures the net change in supply and demand at the best bid and ask across successive quotes (Cont, Kukanov & Stoikov, 2014).
As US_COMP is a composite, the NBBO tick type is used rather than QTE.

import onetick.py as otp

# Retrieve NBBO quote data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='NBBO')
data = data[['BID_PRICE', 'BID_SIZE', 'ASK_PRICE', 'ASK_SIZE']]

# Prior bid and ask prices / sizes (col[-1] is the previous tick's value)
data['PRIOR_BID_PRICE'] = data['BID_PRICE'][-1]
data['PRIOR_BID_SIZE'] = data['BID_SIZE'][-1]
data['PRIOR_ASK_PRICE'] = data['ASK_PRICE'][-1]
data['PRIOR_ASK_SIZE'] = data['ASK_SIZE'][-1]

# Bid flow
def bid_flow(r):
    if r['BID_PRICE'] > r['PRIOR_BID_PRICE']:
        return r['BID_SIZE']
    if r['BID_PRICE'] < r['PRIOR_BID_PRICE']:
        return r['PRIOR_BID_SIZE']
    return r['BID_SIZE'] - r['PRIOR_BID_SIZE']

data['BID_FLOW'] = data.apply(bid_flow)

# Ask flow
def ask_flow(r):
    if r['ASK_PRICE'] > r['PRIOR_ASK_PRICE']:
        return r['ASK_SIZE']
    if r['ASK_PRICE'] < r['PRIOR_ASK_PRICE']:
        return r['PRIOR_ASK_SIZE']
    return r['ASK_SIZE'] - r['PRIOR_ASK_SIZE']

data['ASK_FLOW'] = data.apply(ask_flow)

# Order Flow Imbalance
data['OFI'] = data['BID_FLOW'] - data['ASK_FLOW']

data = data[['BID_PRICE', 'ASK_PRICE', 'BID_FLOW', 'ASK_FLOW', 'OFI']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time BID_PRICE ASK_PRICE BID_FLOW ASK_FLOW OFI
0 2024-01-03 09:30:00.001830617 50.00 50.18 9 2 7
1 2024-01-03 09:30:00.002215206 50.00 50.18 0 0 0
2 2024-01-03 09:30:00.002524728 50.00 50.18 0 1 -1
3 2024-01-03 09:30:00.003295288 50.00 50.18 1 0 1
4 2024-01-03 09:30:00.010072539 50.00 50.18 0 1 -1
... ... ... ... ... ... ...
995 2024-01-03 09:30:03.909674185 50.18 50.20 0 0 0
996 2024-01-03 09:30:03.909758379 50.18 50.19 0 21 -21
997 2024-01-03 09:30:03.909883162 50.18 50.20 0 21 -21
998 2024-01-03 09:30:03.910223796 50.18 50.20 0 0 0
999 2024-01-03 09:30:03.910328511 50.18 50.20 0 0 0

1000 rows × 6 columns

Realized Volatility#

Realized Volatility (RV) from Trade Data.
Trades are first bucketed into 1-minute periods, keeping the last price of each period.
Log returns are calculated as LOG(LAST_PRICE / previous LAST_PRICE).

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Divide into 1-minute periods, keeping the last price of each minute
data = data.agg({'LAST_PRICE': otp.agg.last('PRICE')}, bucket_interval=otp.Minute(1))

# Previous minute's last price and the log return
# (LAST_PRICE[-1] is the previous minute's last price)
data['PRIOR_LAST_PRICE'] = data['LAST_PRICE'][-1]
data['LOG_RETURN'] = otp.math.log(data['LAST_PRICE'] / data['PRIOR_LAST_PRICE'])

# Rolling 30-minute standard deviation of the log returns
data = data.agg(
    {'ROLLING_STDDEV_LOG_RETURN': otp.agg.stddev('LOG_RETURN')},
    running=True,
    bucket_interval=otp.Minute(30),
    all_fields=True
)

# Annualize: 252 trading days * 13 thirty-minute periods per day
data['ANNUALIZED_RV'] = data['ROLLING_STDDEV_LOG_RETURN'] * 252 * 13

data = data[['LOG_RETURN', 'ROLLING_STDDEV_LOG_RETURN', 'ANNUALIZED_RV']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LOG_RETURN ROLLING_STDDEV_LOG_RETURN ANNUALIZED_RV
0 2024-01-03 09:31:00 NaN NaN NaN
1 2024-01-03 09:32:00 -0.001397 0.000000 0.000000
2 2024-01-03 09:33:00 -0.000999 0.000199 0.651608
3 2024-01-03 09:34:00 -0.000300 0.000453 1.485066
4 2024-01-03 09:35:00 -0.000100 0.000523 1.713689
... ... ... ... ...
385 2024-01-03 15:56:00 0.000691 0.000535 1.752873
386 2024-01-03 15:57:00 0.000198 0.000534 1.748094
387 2024-01-03 15:58:00 0.000099 0.000525 1.718966
388 2024-01-03 15:59:00 0.000198 0.000525 1.721012
389 2024-01-03 16:00:00 -0.000198 0.000507 1.662270

390 rows × 4 columns

Realized Volatility on 1 Min Bars#

Realized Volatility (RV) from 1 Minute Trade Bars.
Log returns are calculated on the bar LAST as LOG(LAST / previous LAST).
A 30-minute rolling standard deviation of the log returns is calculated.
The result is annualized by multiplying by the number of trading days (252) and the number of 30-minute periods in a trading day (13).

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST']]

# Previous bar's last price and the log return (LAST[-1] is the previous bar's LAST)
data['PRIOR_LAST'] = data['LAST'][-1]
data['LOG_RETURN'] = otp.math.log(data['LAST'] / data['PRIOR_LAST'])

# Rolling 30-minute standard deviation of the log returns
data = data.agg(
    {'ROLLING_STDDEV_LOG_RETURN': otp.agg.stddev('LOG_RETURN')},
    running=True,
    bucket_interval=otp.Minute(30),
    all_fields=True
)

# Annualize: 252 trading days * 13 thirty-minute periods per day
data['ANNUALIZED_RV'] = data['ROLLING_STDDEV_LOG_RETURN'] * 252 * 13

data = data[['LOG_RETURN', 'ROLLING_STDDEV_LOG_RETURN', 'ANNUALIZED_RV']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LOG_RETURN ROLLING_STDDEV_LOG_RETURN ANNUALIZED_RV
0 2024-01-03 09:31:00 NaN NaN NaN
1 2024-01-03 09:32:00 -0.001397 0.000000 0.000000
2 2024-01-03 09:33:00 -0.000999 0.000199 0.651608
3 2024-01-03 09:34:00 -0.000300 0.000453 1.485066
4 2024-01-03 09:35:00 -0.000100 0.000523 1.713689
... ... ... ... ...
384 2024-01-03 15:55:00 -0.000396 0.000485 1.588239
385 2024-01-03 15:56:00 0.000594 0.000495 1.623197
386 2024-01-03 15:57:00 0.000198 0.000494 1.618035
387 2024-01-03 15:58:00 0.000099 0.000485 1.589154
388 2024-01-03 15:59:00 0.000198 0.000486 1.592090

389 rows × 4 columns

Rate of Change (ROC)#

Rate of Change (ROC) Indicator from Trade Data.
Returns the Rate of Change (ROC) indicator over a lookback period of 7 seconds.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Prevailing price at the start of the 7-second window
data = data.agg(
    {'PRICE_N_BACK': otp.agg.first('PRICE')},
    running=True,
    bucket_interval=otp.Second(7),
    all_fields=True
)

# Rate of Change
data['ROC'] = 100 * (data['PRICE'] - data['PRICE_N_BACK']) / data['PRICE_N_BACK']

data = data[['PRICE', 'PRICE_N_BACK', 'ROC']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE PRICE_N_BACK ROC
0 2024-01-03 09:30:00.065443591 50.02 50.02 0.000000
1 2024-01-03 09:30:00.111130049 50.16 50.02 0.279888
2 2024-01-03 09:30:00.127459523 50.17 50.02 0.299880
3 2024-01-03 09:30:00.128498068 50.17 50.02 0.299880
4 2024-01-03 09:30:00.135190071 50.13 50.02 0.219912
... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 50.01 0.219956
996 2024-01-03 09:30:07.378683771 50.12 50.01 0.219956
997 2024-01-03 09:30:07.379088757 50.12 50.01 0.219956
998 2024-01-03 09:30:07.390014965 50.12 50.01 0.219956
999 2024-01-03 09:30:07.396341167 50.09 50.01 0.159968

1000 rows × 4 columns

Rate of Change (ROC) on 1 Min Bars#

Rate of Change (ROC) Indicator from 1 Minute Trade Bars.
Returns the Rate of Change (ROC) indicator over a lookback of 7 one-minute bars.
LAST_N_BACK is the bar LAST 7 bars earlier.

 ROC = 100 * (LAST - LAST_N_BACK) / LAST_N_BACK
import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST']]

# Bar LAST 7 bars earlier (the first LAST in a running 7-bar window)
data = data.agg(
    {'LAST_N_BACK': otp.agg.first('LAST')},
    running=True,
    bucket_interval=7,
    bucket_units='ticks',
    all_fields=True
)

# Rate of Change
data['ROC'] = 100 * (data['LAST'] - data['LAST_N_BACK']) / data['LAST_N_BACK']

data = data[['LAST', 'LAST_N_BACK', 'ROC']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST LAST_N_BACK ROC
0 2024-01-03 09:31:00 50.160 50.160 0.000000
1 2024-01-03 09:32:00 50.090 50.160 -0.139553
2 2024-01-03 09:33:00 50.040 50.160 -0.239234
3 2024-01-03 09:34:00 50.025 50.160 -0.269139
4 2024-01-03 09:35:00 50.020 50.160 -0.279107
... ... ... ... ...
384 2024-01-03 15:55:00 50.475 50.655 -0.355345
385 2024-01-03 15:56:00 50.505 50.600 -0.187747
386 2024-01-03 15:57:00 50.515 50.525 -0.019792
387 2024-01-03 15:58:00 50.520 50.510 0.019798
388 2024-01-03 15:59:00 50.530 50.540 -0.019786

389 rows × 4 columns

Rolling Stddev#

Rolling Standard Deviation from Trade Data.
Returns the Rolling Standard Deviation of trade PRICE.
The period is defined as 5 minutes.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Rolling 5-minute standard deviation of PRICE
data = data.agg(
    {'ROLLING_STDDEV_PRICE': otp.agg.stddev('PRICE')},
    running=True,
    bucket_interval=otp.Minute(5),
    all_fields=True
)

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE ROLLING_STDDEV_PRICE
0 2024-01-03 09:30:00.065443591 50.02 0.000000
1 2024-01-03 09:30:00.111130049 50.16 0.070000
2 2024-01-03 09:30:00.127459523 50.17 0.068475
3 2024-01-03 09:30:00.128498068 50.17 0.063640
4 2024-01-03 09:30:00.135190071 50.13 0.056921
... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 0.049396
996 2024-01-03 09:30:07.378683771 50.12 0.049373
997 2024-01-03 09:30:07.379088757 50.12 0.049350
998 2024-01-03 09:30:07.390014965 50.12 0.049327
999 2024-01-03 09:30:07.396341167 50.09 0.049321

1000 rows × 3 columns

Rolling Stddev on 1 Min Bars#

Rolling Standard Deviation from 1 Minute Trade Bars.
Returns the rolling standard deviation of the bar LAST price.
The period is defined as 5 one-minute bars.

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST']]

# Rolling 5-bar standard deviation of the bar LAST price
data = data.agg(
    {'ROLLING_STDDEV_PRICE': otp.agg.stddev('LAST')},
    running=True,
    bucket_interval=5,
    bucket_units='ticks',
    all_fields=True
)

data = data[['LAST', 'ROLLING_STDDEV_PRICE']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST ROLLING_STDDEV_PRICE
0 2024-01-03 09:31:00 50.160 0.000000
1 2024-01-03 09:32:00 50.090 0.035000
2 2024-01-03 09:33:00 50.040 0.049216
3 2024-01-03 09:34:00 50.025 0.052723
4 2024-01-03 09:35:00 50.020 0.052688
... ... ... ...
384 2024-01-03 15:55:00 50.475 0.022672
385 2024-01-03 15:56:00 50.505 0.021213
386 2024-01-03 15:57:00 50.515 0.021541
387 2024-01-03 15:58:00 50.520 0.016000
388 2024-01-03 15:59:00 50.530 0.018815

389 rows × 3 columns

RSI#

RSI Indicator from Trade Data.
Returns the RSI together with the RS (Average Gain over Average Loss).
The previous tick’s PRICE (PRICE[-1]) is used to calculate the change in price.
The change is separated into a GAIN (positive changes) and a LOSS (absolute of negative changes).

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Previous price and change in price (PRICE[-1] is the previous tick's price)
data['PRIOR_PRICE'] = data['PRICE'][-1]
data['CHANGE_PRICE'] = data['PRICE'] - data['PRIOR_PRICE']

# Separate the change into gains and losses
data['GAIN'] = data.apply(lambda r: r['CHANGE_PRICE'] if r['CHANGE_PRICE'] > 0 else 0)
data['LOSS'] = data.apply(lambda r: -r['CHANGE_PRICE'] if r['CHANGE_PRICE'] < 0 else 0)

# Rolling 14-minute average gain and loss
data = data.agg(
    {
        'MVG_AVG_GAIN': otp.agg.average('GAIN'),
        'MVG_AVG_LOSS': otp.agg.average('LOSS'),
    },
    running=True,
    bucket_interval=otp.Minute(14),
    all_fields=True
)

# RS and RSI
data['RS'] = data['MVG_AVG_GAIN'] / data['MVG_AVG_LOSS']
data['RSI'] = 100 - (100 / (1 + data['RS']))

data = data[['PRICE', 'RS', 'RSI', 'MVG_AVG_GAIN', 'MVG_AVG_LOSS']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE RS RSI MVG_AVG_GAIN MVG_AVG_LOSS
0 2024-01-03 09:30:00.065443591 50.02 NaN NaN 0.000000 NaN
1 2024-01-03 09:30:00.111130049 50.16 inf 100.000000 0.070000 0.000000
2 2024-01-03 09:30:00.127459523 50.17 inf 100.000000 0.050000 0.000000
3 2024-01-03 09:30:00.128498068 50.17 inf 100.000000 0.037500 0.000000
4 2024-01-03 09:30:00.135190071 50.13 3.000000 75.000000 0.030000 0.010000
... ... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 1.012996 50.322792 0.007265 0.007172
996 2024-01-03 09:30:07.378683771 50.12 1.012997 50.322817 0.007258 0.007165
997 2024-01-03 09:30:07.379088757 50.12 1.012998 50.322842 0.007250 0.007157
998 2024-01-03 09:30:07.390014965 50.12 1.012999 50.322867 0.007243 0.007150
999 2024-01-03 09:30:07.396341167 50.09 1.008759 50.218013 0.007236 0.007173

1000 rows × 6 columns

RSI on 1 Min Bars#

RSI Indicator from 1 Minute Trade Bars.
Returns the RSI together with the RS (Average Gain over Average Loss).
The previous bar’s LAST (LAST[-1]) is used to calculate the change in price.
The change is separated into a GAIN (positive changes) and a LOSS (absolute of negative changes).
The gains and losses are averaged across a rolling 14-minute period.

RS  = MVG_AVG_GAIN / MVG_AVG_LOSS
RSI = 100 - (100 / (1 + RS))
import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST']]

# Previous bar's last price and the change in price (LAST[-1] is the previous bar's LAST)
data['PRIOR_LAST'] = data['LAST'][-1]
data['CHANGE_LAST'] = data['LAST'] - data['PRIOR_LAST']

# Separate the change into gains and losses
data['GAIN'] = data.apply(lambda r: r['CHANGE_LAST'] if r['CHANGE_LAST'] > 0 else 0)
data['LOSS'] = data.apply(lambda r: -r['CHANGE_LAST'] if r['CHANGE_LAST'] < 0 else 0)

# Rolling 14-minute average gain and loss
data = data.agg(
    {
        'MVG_AVG_GAIN': otp.agg.average('GAIN'),
        'MVG_AVG_LOSS': otp.agg.average('LOSS'),
    },
    running=True,
    bucket_interval=otp.Minute(14),
    all_fields=True
)

# RS and RSI
data['RS'] = data['MVG_AVG_GAIN'] / data['MVG_AVG_LOSS']
data['RSI'] = 100 - (100 / (1 + data['RS']))

data = data[['LAST', 'RS', 'RSI', 'MVG_AVG_GAIN', 'MVG_AVG_LOSS']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST RS RSI MVG_AVG_GAIN MVG_AVG_LOSS
0 2024-01-03 09:31:00 50.160 NaN NaN 0.000000 NaN
1 2024-01-03 09:32:00 50.090 0.000000 0.000000 0.000000 0.070000
2 2024-01-03 09:33:00 50.040 0.000000 0.000000 0.000000 0.060000
3 2024-01-03 09:34:00 50.025 0.000000 0.000000 0.000000 0.045000
4 2024-01-03 09:35:00 50.020 0.000000 0.000000 0.000000 0.035000
... ... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 0.373379 27.186910 0.006171 0.016529
385 2024-01-03 15:56:00 50.505 0.487900 32.791170 0.008064 0.016529
386 2024-01-03 15:57:00 50.515 0.539272 35.034208 0.008779 0.016279
387 2024-01-03 15:58:00 50.520 0.573800 36.459521 0.009136 0.015921
388 2024-01-03 15:59:00 50.530 0.641395 39.076226 0.009850 0.015357

389 rows × 6 columns

Stochastic Oscillator#

Stochastic Oscillator from Trade Data.
Uses a 14-minute rolling Minimum to calculate MLOW (lowest price traded in the period).
Uses a 14-minute rolling Maximum to calculate MHIGH (highest price traded in the period).

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE']]

# Rolling 14-minute minimum (MLOW) and maximum (MHIGH) prices
data = data.agg(
    {
        'MLOW': otp.agg.min('PRICE'),
        'MHIGH': otp.agg.max('PRICE'),
    },
    running=True,
    bucket_interval=otp.Minute(14),
    all_fields=True
)

# %K: position of the current price within the 14-minute high/low range
data['PCNT_K'] = 100 * (data['PRICE'] - data['MLOW']) / (data['MHIGH'] - data['MLOW'])

# %D: 3-minute moving average of %K
data = data.agg(
    {'PCNT_D': otp.agg.average('PCNT_K')},
    running=True,
    bucket_interval=otp.Minute(3),
    all_fields=True
)

data = data[['PRICE', 'MLOW', 'MHIGH', 'PCNT_K', 'PCNT_D']]

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 30),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE MLOW MHIGH PCNT_K PCNT_D
0 2024-01-03 09:30:00.065443591 50.02 50.020 50.02 NaN NaN
1 2024-01-03 09:30:00.111130049 50.16 50.020 50.16 100.000000 100.000000
2 2024-01-03 09:30:00.127459523 50.17 50.020 50.17 100.000000 100.000000
3 2024-01-03 09:30:00.128498068 50.17 50.020 50.17 100.000000 100.000000
4 2024-01-03 09:30:00.135190071 50.13 50.020 50.17 73.333333 93.333333
... ... ... ... ... ... ...
995 2024-01-03 09:30:07.378682043 50.12 50.002 50.23 51.754386 66.419446
996 2024-01-03 09:30:07.378683771 50.12 50.002 50.23 51.754386 66.404722
997 2024-01-03 09:30:07.379088757 50.12 50.002 50.23 51.754386 66.390028
998 2024-01-03 09:30:07.390014965 50.12 50.002 50.23 51.754386 66.375363
999 2024-01-03 09:30:07.396341167 50.09 50.002 50.23 38.596491 66.347556

1000 rows × 6 columns

Stochastic Oscillator on 1 Min Bars#

Stochastic Oscillator from 1 Minute Trade Bars.
Uses a 14-bar rolling minimum of the bar LOW to calculate MLOW (lowest price in the period).
Uses a 14-bar rolling maximum of the bar HIGH to calculate MHIGH (highest price in the period).

%K = 100 * (LAST - MLOW) / (MHIGH - MLOW)
%D = 3-minute moving average of %K
import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['HIGH', 'LOW', 'LAST']]

# Rolling 14-bar minimum LOW (MLOW) and maximum HIGH (MHIGH)
data = data.agg(
    {
        'MLOW': otp.agg.min('LOW'),
        'MHIGH': otp.agg.max('HIGH'),
    },
    running=True,
    bucket_interval=14,
    bucket_units='ticks',
    all_fields=True
)

# %K: position of the current bar LAST within the 14-bar high/low range
data['PCNT_K'] = 100 * (data['LAST'] - data['MLOW']) / (data['MHIGH'] - data['MLOW'])

# %D: 3-minute moving average of %K
data = data.agg(
    {'PCNT_D': otp.agg.average('PCNT_K')},
    running=True,
    bucket_interval=otp.Minute(3),
    all_fields=True
)

data = data[['LAST', 'MLOW', 'MHIGH', 'PCNT_K', 'PCNT_D']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST MLOW MHIGH PCNT_K PCNT_D
0 2024-01-03 09:31:00 50.160 50.002 50.22 72.477064 72.477064
1 2024-01-03 09:32:00 50.090 50.002 50.22 40.366972 56.422018
2 2024-01-03 09:33:00 50.040 50.002 50.22 17.431193 43.425076
3 2024-01-03 09:34:00 50.025 50.000 50.22 11.363636 23.053934
4 2024-01-03 09:35:00 50.020 50.000 50.22 9.090909 12.628579
... ... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 50.470 50.68 2.380952 9.078251
385 2024-01-03 15:56:00 50.505 50.470 50.68 16.666667 7.226399
386 2024-01-03 15:57:00 50.515 50.470 50.68 21.428571 13.492063
387 2024-01-03 15:58:00 50.520 50.470 50.68 23.809524 20.634921
388 2024-01-03 15:59:00 50.530 50.470 50.68 28.571429 24.603175

389 rows × 6 columns

Volume Bars#

Volume Bars (Fixed Volume Bins) from Trade Data.
The cumulative volume is calculated across the period and floored into fixed-size bins:

VOL_BIN = FLOOR(cumulative volume / 100000)  (a new bar every 100,000 shares)

The day is then aggregated grouped by VOL_BIN, producing OHLC, count and volume per bar.
Because bars span different time ranges, the start and end time of each bar are also returned.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Cumulative volume, then floor into fixed 100,000-share volume bins
data = data.agg({'ACC_VOLUME': otp.agg.sum('SIZE')}, running=True, all_fields=True)
data['VOL_BIN'] = otp.math.floor(data['ACC_VOLUME'] / 100000)

# Aggregate each volume bin into an OHLC bar
data = data.agg(
    {
        'BIN_START': otp.agg.first('Time'),
        'BIN_END': otp.agg.last('Time'),
        'FIRST': otp.agg.first('PRICE'),
        'HIGH': otp.agg.max('PRICE'),
        'LOW': otp.agg.min('PRICE'),
        'LAST': otp.agg.last('PRICE'),
        'TRADE_COUNT': otp.agg.count(),
        'VOLUME': otp.agg.sum('SIZE'),
    },
    group_by=['VOL_BIN']
)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time VOL_BIN FIRST HIGH LOW LAST TRADE_COUNT VOLUME BIN_START BIN_END
0 2024-01-03 16:00:00 0.0 50.020 50.170 50.002 50.0850 75 1785 2024-01-03 09:30:00.065443591 2024-01-03 09:30:00.801003556
1 2024-01-03 16:00:00 2.0 50.090 50.090 50.090 50.0900 1 262623 2024-01-03 09:30:00.887879969 2024-01-03 09:30:00.887879969
2 2024-01-03 16:00:00 5.0 50.090 50.230 50.050 50.1500 1654 335376 2024-01-03 09:30:00.888066374 2024-01-03 09:30:29.305168688
3 2024-01-03 16:00:00 6.0 50.160 50.190 50.050 50.0500 1162 100177 2024-01-03 09:30:29.305775276 2024-01-03 09:32:55.319732421
4 2024-01-03 16:00:00 7.0 50.050 50.100 49.990 49.9900 1084 99354 2024-01-03 09:32:55.319733020 2024-01-03 09:35:19.495008777
... ... ... ... ... ... ... ... ... ... ...
143 2024-01-03 16:00:00 146.0 50.550 50.555 50.540 50.5500 431 99006 2024-01-03 15:59:28.015701322 2024-01-03 15:59:33.265693472
144 2024-01-03 16:00:00 147.0 50.550 50.560 50.545 50.5550 389 102028 2024-01-03 15:59:33.265693538 2024-01-03 15:59:40.294178279
145 2024-01-03 16:00:00 148.0 50.560 50.560 50.530 50.5301 649 97956 2024-01-03 15:59:40.294179336 2024-01-03 15:59:53.856498573
146 2024-01-03 16:00:00 149.0 50.535 50.540 50.520 50.5200 275 101461 2024-01-03 15:59:53.857395780 2024-01-03 15:59:59.970955843
147 2024-01-03 16:00:00 150.0 50.521 50.521 50.520 50.5200 3 9620 2024-01-03 15:59:59.971309605 2024-01-03 15:59:59.994802751

148 rows × 10 columns

Volume Profile#

Volume Profile (Volume Histogram) from Trade Data.
Retrieves the VOLUME and TRADE_COUNT grouped by PRICE.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Total volume and trade count for each distinct price level
data = data.agg(
    {
        'VOLUME': otp.agg.sum('SIZE'),
        'TRADE_COUNT': otp.agg.count(),
    },
    group_by=['PRICE']
)

# Return first 1000 rows
data = data.limit(1000)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE VOLUME TRADE_COUNT
0 2024-01-03 16:00:00 49.9400 16 5
1 2024-01-03 16:00:00 49.9401 30 2
2 2024-01-03 16:00:00 49.9410 100 1
3 2024-01-03 16:00:00 49.9420 100 1
4 2024-01-03 16:00:00 49.9426 1 1
... ... ... ... ...
995 2024-01-03 16:00:00 50.1867 3 2
996 2024-01-03 16:00:00 50.1868 35 1
997 2024-01-03 16:00:00 50.1869 110 4
998 2024-01-03 16:00:00 50.1870 349 3
999 2024-01-03 16:00:00 50.1871 103 2

1000 rows × 4 columns

Volume Profile by Sample Bins#

Volume Profile by Sample from Trade Data.
The price range across the trading day is divided into a fixed number of samples (here 100).

TICK_SIZE = (max price - min price) / (samples - 1) = range / 99.

Each trade’s price is floored into a PRICE_BIN by dividing by TICK_SIZE.
Retrieves the VOLUME and TRADE_COUNT grouped by PRICE_BIN.

import onetick.py as otp

SAMPLES = 100

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Compute the day's price range with a running (cumulative) min/max so the
# TICK_SIZE is available on every tick without a separate join.
data = data.agg(
    {
        'MIN_PRICE': otp.agg.min('PRICE'),
        'MAX_PRICE': otp.agg.max('PRICE'),
    },
    running=True,
    all_fields=True
)
data['TICK_SIZE'] = (data['MAX_PRICE'] - data['MIN_PRICE']) / (SAMPLES - 1)

# Floor each price into its sample bin
data['PRICE_BIN'] = otp.math.floor(data['PRICE'] / data['TICK_SIZE'])

# Total volume and trade count for each price bin
data = data.agg(
    {
        'VOLUME': otp.agg.sum('SIZE'),
        'TRADE_COUNT': otp.agg.count(),
    },
    group_by=['PRICE_BIN']
)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE_BIN VOLUME TRADE_COUNT
0 2024-01-03 16:00:00 5767.0 7857 87
1 2024-01-03 16:00:00 5768.0 83268 348
2 2024-01-03 16:00:00 5769.0 36802 346
3 2024-01-03 16:00:00 5770.0 73483 568
4 2024-01-03 16:00:00 5771.0 68848 785
... ... ... ... ...
523 2024-01-03 16:00:00 33033.0 83 1
524 2024-01-03 16:00:00 33085.0 163 2
525 2024-01-03 16:00:00 33112.0 105 2
526 2024-01-03 16:00:00 35470.0 3 1
527 2024-01-03 16:00:00 inf 2 1

528 rows × 4 columns

Volume Profile by Tick Size Bins#

Volume Profile by Tick Size from Trade Data.
A fixed tick size is specified (here 1 cent, 0.01).
Each trade’s price is floored to that tick size to form a PRICE_BIN.
Retrieves the VOLUME and TRADE_COUNT grouped by PRICE_BIN.

import onetick.py as otp

TICK_SIZE = 0.01

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Floor each price to the nearest tick-size bin
data['PRICE_BIN'] = otp.math.floor(data['PRICE'] / TICK_SIZE) * TICK_SIZE

# Total volume and trade count for each price bin
data = data.agg(
    {
        'VOLUME': otp.agg.sum('SIZE'),
        'TRADE_COUNT': otp.agg.count(),
    },
    group_by=['PRICE_BIN']
)

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time PRICE_BIN VOLUME TRADE_COUNT
0 2024-01-03 16:00:00 49.94 766 19
1 2024-01-03 16:00:00 49.95 1262 27
2 2024-01-03 16:00:00 49.96 3982 55
3 2024-01-03 16:00:00 49.97 2728 49
4 2024-01-03 16:00:00 49.98 7869 116
... ... ... ... ...
72 2024-01-03 16:00:00 50.66 162270 1430
73 2024-01-03 16:00:00 50.67 81742 774
74 2024-01-03 16:00:00 50.68 16980 144
75 2024-01-03 16:00:00 50.74 19 1
76 2024-01-03 16:00:00 50.80 81 1

77 rows × 4 columns

Volume Spike Detection#

Volume Spike Detection from Trade Data.
Volume is aggregated into 1-minute buckets.
MAVG_VOLUME is the average volume across the last 5 buckets (current + 4 preceding).
A spike is flagged (SPIKES = 1) when the current bucket volume exceeds twice MAVG_VOLUME.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Bucket volume, last price and trade count per 1-minute interval
data = data.agg(
    {
        'LAST_PRICE': otp.agg.last('PRICE'),
        'VOLUME': otp.agg.sum('SIZE'),
        'TRADE_COUNT': otp.agg.count(),
    },
    bucket_interval=otp.Minute(1)
)

# Moving average volume over the last 5 buckets (current + 4 preceding)
data = data.agg(
    {'MAVG_VOLUME': otp.agg.average('VOLUME')},
    running=True,
    bucket_interval=5,
    bucket_units='ticks',
    all_fields=True
)

# Flag spikes where volume is more than twice the moving-average volume
data['SPIKES'] = data.apply(lambda r: 1 if r['VOLUME'] > 2 * r['MAVG_VOLUME'] else 0)

data = data[['LAST_PRICE', 'VOLUME', 'TRADE_COUNT', 'MAVG_VOLUME', 'SPIKES']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST_PRICE VOLUME TRADE_COUNT MAVG_VOLUME SPIKES
0 2024-01-03 09:31:00 50.160 624483 1988 624483.000000 0
1 2024-01-03 09:32:00 50.090 45575 529 335029.000000 0
2 2024-01-03 09:33:00 50.040 34353 410 234803.666667 0
3 2024-01-03 09:34:00 50.025 37577 383 185497.000000 0
4 2024-01-03 09:35:00 50.020 37479 402 155893.400000 0
... ... ... ... ... ... ...
385 2024-01-03 15:56:00 50.505 202820 1538 214195.600000 0
386 2024-01-03 15:57:00 50.515 183148 1429 216602.000000 0
387 2024-01-03 15:58:00 50.520 364420 1878 249589.200000 0
388 2024-01-03 15:59:00 50.530 178199 1095 223255.000000 0
389 2024-01-03 16:00:00 50.520 658118 3149 317341.000000 1

390 rows × 6 columns

Volume Spike Detection on 1 Min Bars#

Volume Spike Detection from 1 Minute Trade Bars.
MAVG_VOLUME is the average of the bar VOLUME across the last 5 bars (current + 4 preceding).
A spike is flagged (SPIKES = 1) when the current bar VOLUME exceeds twice MAVG_VOLUME.

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST', 'VOLUME']]

# Moving average volume over the last 5 bars (current + 4 preceding)
data = data.agg(
    {'MAVG_VOLUME': otp.agg.average('VOLUME')},
    running=True,
    bucket_interval=5,
    bucket_units='ticks',
    all_fields=True
)

# Flag spikes where volume is more than twice the moving-average volume
data['SPIKES'] = data.apply(lambda r: 1 if r['VOLUME'] > 2 * r['MAVG_VOLUME'] else 0)

data = data[['LAST', 'VOLUME', 'MAVG_VOLUME', 'SPIKES']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST VOLUME MAVG_VOLUME SPIKES
0 2024-01-03 09:31:00 50.160 69657 69657.0 0
1 2024-01-03 09:32:00 50.090 39792 54724.5 0
2 2024-01-03 09:33:00 50.040 26904 45451.0 0
3 2024-01-03 09:34:00 50.025 32673 42256.5 0
4 2024-01-03 09:35:00 50.020 32783 40361.8 0
... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 169383 210648.8 0
385 2024-01-03 15:56:00 50.505 185335 198229.0 0
386 2024-01-03 15:57:00 50.515 166268 200247.0 0
387 2024-01-03 15:58:00 50.520 346983 233206.0 0
388 2024-01-03 15:59:00 50.530 166368 206867.4 0

389 rows × 5 columns

Volume Surge Indicator#

Volume Surge Indicator from Trade Data.
Volume is aggregated into 1-minute buckets.
MAVG_VOLUME is the average volume across the last 100 buckets (current + 99 preceding).
VOLUME_SURGE = 100 * VOLUME / MAVG_VOLUME expresses the current volume relative to the recent average.

import onetick.py as otp

# Retrieve trade data for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE', tick_type='TRD')
data = data[['PRICE', 'SIZE']]

# Bucket volume, last price and trade count per 1-minute interval
data = data.agg(
    {
        'LAST_PRICE': otp.agg.last('PRICE'),
        'VOLUME': otp.agg.sum('SIZE'),
        'TRADE_COUNT': otp.agg.count(),
    },
    bucket_interval=otp.Minute(1)
)

# Moving average volume over the last 100 buckets (current + 99 preceding)
data = data.agg(
    {'MAVG_VOLUME': otp.agg.average('VOLUME')},
    running=True,
    bucket_interval=100,
    bucket_units='ticks',
    all_fields=True
)

# Volume surge: current volume as a percentage of the moving-average volume
data['VOLUME_SURGE'] = 100 * data['VOLUME'] / data['MAVG_VOLUME']

data = data[['LAST_PRICE', 'VOLUME', 'TRADE_COUNT', 'MAVG_VOLUME', 'VOLUME_SURGE']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST_PRICE VOLUME TRADE_COUNT MAVG_VOLUME VOLUME_SURGE
0 2024-01-03 09:31:00 50.160 624483 1988 624483.000000 100.000000
1 2024-01-03 09:32:00 50.090 45575 529 335029.000000 13.603300
2 2024-01-03 09:33:00 50.040 34353 410 234803.666667 14.630521
3 2024-01-03 09:34:00 50.025 37577 383 185497.000000 20.257470
4 2024-01-03 09:35:00 50.020 37479 402 155893.400000 24.041428
... ... ... ... ... ... ...
385 2024-01-03 15:56:00 50.505 202820 1538 52276.230000 387.977480
386 2024-01-03 15:57:00 50.515 183148 1429 53690.820000 341.116042
387 2024-01-03 15:58:00 50.520 364420 1878 57021.490000 639.092384
388 2024-01-03 15:59:00 50.530 178199 1095 58651.630000 303.826168
389 2024-01-03 16:00:00 50.520 658118 3149 64882.060000 1014.329693

390 rows × 6 columns

Volume Surge Indicator on 1 Min Bars#

Volume Surge Indicator from 1 Minute Trade Bars.
MAVG_VOLUME is the average of the bar VOLUME across the last 100 bars (current + 99 preceding).
VOLUME_SURGE = 100 * VOLUME / MAVG_VOLUME expresses the current bar volume relative to the recent average.

import onetick.py as otp

# Retrieve 1-minute trade bars for CSCO
data = otp.DataSource(db='US_COMP_SAMPLE_BARS', tick_type='TRD_1M')
data = data[['LAST', 'VOLUME', 'TRADE_TICK_COUNT']]

# Moving average volume over the last 100 bars (current + 99 preceding)
data = data.agg(
    {'MAVG_VOLUME': otp.agg.average('VOLUME')},
    running=True,
    bucket_interval=100,
    bucket_units='ticks',
    all_fields=True
)

# Volume surge: current bar volume as a percentage of the moving-average volume
data['VOLUME_SURGE'] = 100 * data['VOLUME'] / data['MAVG_VOLUME']

data = data[['LAST', 'VOLUME', 'TRADE_TICK_COUNT', 'MAVG_VOLUME', 'VOLUME_SURGE']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 9, 30),
    end=otp.dt(2024, 1, 3, 16, 0),
    timezone='America/New_York',
    symbols='CSCO'
)

result
Time LAST VOLUME TRADE_TICK_COUNT MAVG_VOLUME VOLUME_SURGE
0 2024-01-03 09:31:00 50.160 69657 418 69657.00 100.000000
1 2024-01-03 09:32:00 50.090 39792 244 54724.50 72.713319
2 2024-01-03 09:33:00 50.040 26904 187 45451.00 59.193417
3 2024-01-03 09:34:00 50.025 32673 149 42256.50 77.320649
4 2024-01-03 09:35:00 50.020 32783 173 40361.80 81.222839
... ... ... ... ... ... ...
384 2024-01-03 15:55:00 50.475 169383 787 44883.59 377.382914
385 2024-01-03 15:56:00 50.505 185335 925 46416.08 399.290504
386 2024-01-03 15:57:00 50.515 166268 850 47726.45 348.377053
387 2024-01-03 15:58:00 50.520 346983 1277 50928.83 681.309584
388 2024-01-03 15:59:00 50.530 166368 669 52473.92 317.048926

389 rows × 6 columns

VPIN#

Volume Synchronized Probability of Informed Trading (VPIN) from Trade Data.
Volume bins are formed every 100,000 share.
BUY_VOLUME is SIZE where AGGRESSOR_SIDE = ‘B’, SELL_VOLUME where AGGRESSOR_SIDE = ‘S’.
VPIN is the rolling average of BIN_IMBALANCE over the last 50 bins.

import onetick.py as otp

# Retrieve trade data for VOD (LSE publishes AGGRESSOR_SIDE)
data = otp.DataSource(db='LSE', tick_type='TRD')
data = data[['SIZE', 'AGGRESSOR_SIDE']]

# Split size into buy/sell volume by aggressor side
data['BUY_VOLUME'] = data.apply(lambda r: r['SIZE'] if r['AGGRESSOR_SIDE'] == 'B' else 0)
data['SELL_VOLUME'] = data.apply(lambda r: r['SIZE'] if r['AGGRESSOR_SIDE'] == 'S' else 0)

# Cumulative volume, then floor into fixed 100,000-share volume bins
data = data.agg({'ACC_VOLUME': otp.agg.sum('SIZE')}, running=True, all_fields=True)
data['VOL_BIN'] = otp.math.floor(data['ACC_VOLUME'] / 100000)

# Aggregate each volume bin
data = data.agg(
    {
        'BIN_START': otp.agg.first('Time'),
        'BIN_END': otp.agg.last('Time'),
        'TRADE_COUNT': otp.agg.count(),
        'VOLUME': otp.agg.sum('SIZE'),
        'BUY_VOLUME': otp.agg.sum('BUY_VOLUME'),
        'SELL_VOLUME': otp.agg.sum('SELL_VOLUME'),
    },
    group_by=['VOL_BIN']
)

# Order imbalance within each bin (absolute net volume over total volume)
data['DIFF'] = data['BUY_VOLUME'] - data['SELL_VOLUME']
data['ABS_DIFF'] = data.apply(lambda r: r['DIFF'] if r['DIFF'] >= 0 else -r['DIFF'])
data['BIN_IMBALANCE'] = data['ABS_DIFF'] / data['VOLUME']

# VPIN: rolling average of the bin imbalance over the last 50 bins
data = data.agg(
    {'VPIN': otp.agg.average('BIN_IMBALANCE')},
    running=True,
    bucket_interval=50,
    bucket_units='ticks',
    all_fields=True
)

data = data[['VOL_BIN', 'BIN_START', 'BIN_END', 'TRADE_COUNT', 'VOLUME',
             'BUY_VOLUME', 'SELL_VOLUME', 'BIN_IMBALANCE', 'VPIN']]

result = otp.run(
    data,
    start=otp.dt(2024, 1, 3, 8),
    end=otp.dt(2024, 1, 3, 16),
    timezone='Europe/London',
    symbols='VOD'
)

result
Time VOL_BIN BIN_START BIN_END TRADE_COUNT VOLUME BUY_VOLUME SELL_VOLUME BIN_IMBALANCE VPIN
0 2024-01-03 16:00:00 1.0 2024-01-03 08:00:06.232 2024-01-03 08:00:09.380 5 188102 0 640 0.003402 0.003402
1 2024-01-03 16:00:00 2.0 2024-01-03 08:00:09.943 2024-01-03 08:00:39.920 22 108351 18726 19626 0.008306 0.005854
2 2024-01-03 16:00:00 3.0 2024-01-03 08:00:39.921 2024-01-03 08:01:54.173 163 47376 11357 9223 0.045044 0.018918
3 2024-01-03 16:00:00 4.0 2024-01-03 08:01:55.438 2024-01-03 08:02:34.502 53 155799 119681 26964 0.595107 0.162965
4 2024-01-03 16:00:00 5.0 2024-01-03 08:02:34.619 2024-01-03 08:04:24.229 58 95773 5459 49506 0.459910 0.222354
... ... ... ... ... ... ... ... ... ... ...
196 2024-01-03 16:00:00 674.0 2024-01-03 15:47:27.146 2024-01-03 15:48:14.860 22 96760 711 58735 0.599669 0.299909
197 2024-01-03 16:00:00 675.0 2024-01-03 15:48:14.860 2024-01-03 15:52:36.288 47 101722 15632 70823 0.542567 0.309792
198 2024-01-03 16:00:00 676.0 2024-01-03 15:52:45.542 2024-01-03 15:55:34.314 48 102397 55608 19929 0.348438 0.299804
199 2024-01-03 16:00:00 677.0 2024-01-03 15:55:40.243 2024-01-03 15:57:04.639 37 100372 25748 71550 0.456322 0.294721
200 2024-01-03 16:00:00 678.0 2024-01-03 15:57:04.639 2024-01-03 15:59:58.296 31 93870 19592 49629 0.319985 0.297549

201 rows × 10 columns