Distinguish Volume from Ticks per the Tick Stream

TLDR: How does Tradovate distinguish actual market orders or traded volume from a tick chart? It appears like the tick price and volume in the packets is a last traded price & volume, so this ends up persisting across packets until new last traded price and volume information arrives. Because of the persistent last traded price and volume, one could inadvertently overestimate the actual quantity of market orders arriving.

I’m attempting identify market orders (volume) from a tick chart series. The Tick Stream panel in the Trader Desktop app seems to appropriately identify market orders (volume) as there are generally more DOM changes than ticks coming through the Tick Stream panel for a given time period (i.e., more limit order changes in the market than there is actual market order volume). Intuitively, that should be the case as the limit order book sees more modifications and activity than the actual traded volume.

To evaluate the issue, I compare a 1-tick chart and 1-volume chart. The 1-tick chart is identical to the one requested by the Trader Desktop app if only a single Tick Stream panel is present (the chart/subscribeQuote request that is also made by the app). However, rather than getting a timestamp-based chart, I go for an equal number of elements, so I can compare like for like. In theory, a 1-volume chart returning 1000 bars should have barTotalVolume >= 1000, and while a 1-tick chart returning 1000 ticks could have any total volume, because we expect more ticks coming from limit order changes at the bid / ask than from traded volume, we should anticipate that tickTotalVolume <= barTotalVolume.

1-Tick Chart


1-Volume Chart


I process the charts using the below code, which is a modification of the example in the documentation. The intent is to process ticks and bars into a nearly-similar format where I can compare bidVolume and offerVolume.

charts?.forEach(({id, td, bars, bp, bt, ts, tks, eoh}) => {
	// Handle bars
	bars?.forEach(obj => {
		// Ensure `timestamp` is accessible as a numerical value for simplicity
		let { open, high, low, close, upVolume, downVolume, upTicks, downTicks, bidVolume, offerVolume} = obj;
		let timestamp = new Date(obj?.timestamp).getTime();
		let bar = { ...obj, timestamp };

		// In a 1-volume bar chart, each bar will contain 1-volume. Orders greater than 1-volume will be sent as multiple bars, sharing the same timestamp. As such, we simply add all observed bars to the state.bars array.


	// Handle ticks
	tks?.forEach(tick => {    
		let {t, p, s, b, a, bs, as} = tick;

		const timestamp = bt + t;   // Actual tick timestamp
		const price = (bp + p) * ts;   // Tick price as contract price

		// Actual bid price as contract price (if bid size defined)
		const bidPrice = bs && ((bp + b) * ts);   

		// Actual ask price as contract price (if ask size defined)
		const askPrice = as && ((bp + a) * ts);    

			id: tick.id,
			timestamp: new Date(timestamp).getTime(),

			price, // Traded price (e.g., market order price)
			size: s, // Tick size (tick volume, that is, traded volume, market order volume)

			bidSize: bs,

			askSize: as,

			// Custom parameters to make it easier to post-process market orders
			// Sell market orders should by definition be less than the ask, but not necessarily equal to the bid if this is the final bid before a price level shift, and vice versa for a buy market order
			offerVolume: (price > bidPrice) ? s : 0, // Buy market orders
			bidVolume: (price < askPrice) ? s : 0 // Sell market orders


What I notice is that the price and size (namely, the tks.<tick>.s field, and thus, a non-zero bidVolume or offerVolume) is present in every tick / message received from the tick chart subscription. This leads me to believe that the tick message is sending the last traded price and volume in every message, even if no actual volume occurred for that particular tick (i.e., the tick was sent to reflect the limit orders at the bid or ask changing, not to reflect volume occurring).

To debug, I total the observed volumes between the same tick and volume charts, as soon as the first batch of 1000 elements was received. Because I’ve given the 1-volume bars the same offerVolume and bidVolume fields as the 1-tick messages, I can compare apples to apples.

// Debug ticks vs. volume bars
// Note: sum(), min(), and max() are custom functions, and work as one would expect

// Accumulate tick info
let numTicks = state.ticks.length;
let tickOfferVolume = sum(state.ticks.map(x=>x.offerVolume));
let tickBidVolume = sum(state.ticks.map(x=>x.bidVolume));
let tickVolumes = state.ticks.map(x=>x.bidVolume + x.offerVolume);
let tickMinVolume = min(tickVolumes);
let tickMinCount = tickVolumes.filter(x=>x===tickMinVolume).length;
let tickMaxVolume = max(tickVolumes);
let tickMinCount = tickVolumes.filter(x=>x===tickMaxVolume).length;
let tickTotalVolume = sum(state.ticks.map(x=>x.bidVolume + x.offerVolume));
// Accumulate bar info
let numBars = state.bars.length;
let barOfferVolume = sum(state.bars.map(x=>x.offerVolume));
let barBidVolume = sum(state.bars.map(x=>x.bidVolume));
let barVolumes = state.bars.map(x=>x.bidVolume + x.offerVolume);
let barMinVolume = min(barVolumes);
let barMinCount = barVolumes.filter(x=>x===barMinVolume).length;
let barMaxVolume = max(barVolumes);
let barMaxCount = barVolumes.filter(x=>x===barMaxVolume).length;
let barTotalVolume = sum(state.bars.map(x=>x.bidVolume + x.offerVolume));
// Log the output
console.log(`${numTicks} Ticks (${tickOfferVolume} A + ${tickBidVolume} B = ${tickTotalVolume} V), Min: ${tickMinCount} ticks x ${tickMinVolume} V, Max: ${tickMaxCount} ticks x ${tickMaxVolume} V\n${numBars} Bars  (${barOfferVolume} A + ${barBidVolume} B = ${barTotalVolume} V), Min: ${barMinCount} bars  x ${barMinVolume} V, Max: ${barMaxCount} bars  x ${barMaxVolume} V`)

I observe that there tend to be less volume from bars than tick messages. Because I see 1000 volume in a 1000 elements load of 1-volume bars, this checks out; however, because I see a total volume of 2038 in the 1000 elements load of 1-tick bars with a minimum tick volume of 1 (it should be 0 if ticks arrive that only contain bid / ask changes), I’m inclined to believe that all ticks contain a volume, even when they shouldn’t, thus indicating that the tick chart shows last traded volume.

1000 Ticks (1399 A + 639 B = 2038 V), Min: 691  ticks x 1 V, Max: 1    ticks x 63 V
1000 Bars  (616  A + 384 B = 1000 V), Min: 1000 bars  x 1 V, Max: 1000 bars  x 1  V

That being said, how does Tradovate determine the actual market orders for the Tick Stream, if not using the 1-volume bars? The quotes returned also seem to exhibit the same behavior as the tick chart (i.e., carrying the last traded price and size, rather than omitting it if no volume during that quote), so it’s unclear to me how the Tick Stream actually functions or if there’s a more elegant way to estimate market order activity.

For my purposes, subscribing to a 1-volume chart is perfectly fine, only complicates my logic slightly. Just trying to understand if there’s a more formal way to determine if a traded price and size in a 1-tick chart is real.