BACK

How to Programmatically calculate Value Area from Time Series Data

Image

Why the Value Area is important

The Value Area (VA) is an important indicator for Day and Swing Traders, because it allows you to identify where institutional investors are buying and selling. Retail traders also mark these levels, and they typically take trades at them too. The levels of a Value are therefore crucial as once a level has been hit, the market can either reverse or continue to move in the same direction. This makes for a good opportunity to take a long or short trade.

These levels are great for timing if you combine then with alerts. When a target level is hit, you can come back to the charts, evaluate the Order Flow and other technicals, and make a decision on whether or not to take a trade.

More about the Value Area

The Value Area is an indicator to show you at which prices the most volume occurred. The gives us more information than the volume shown underneath each candlestick, which only shows volume traded in a specific timeframe.

The Value Area is defined as the place in which 70% of the volume was traded. It is a Premium Indicator on TradingView known as the Fixed Range Volume Profile (FRVP).

Image

The Value Area Calculation

I found the Value Area calculation in the book Mind over Markets: Power Trading With Market Generated Information by James F. Dalton, and you can find an overview of the calculation here.

The kind of timeseries data we will be working with be an Array of Objects of the following interface. The code will be presented in TypeScript.

interface kline {
  id: number,
  symbol: string,
  period: string,
  interval: string,
  start_at: string,
  open_time: string,
  volume: number,
  open: number,
  high: number,
  close: number,
  low: number,
  turnover: number,
}

Steps involved in the calculation

1. Find the total volume, lowest and highest prices:

function sumVolumes(klines) {
  let volumeTotal: number = 0;
  let highest: number = 0;
  let lowest: number = Infinity;

  for (let i = 0; i < klines.length; i++) {
    const { volume, high, low }: { volume: number, high: number, low: number } = klines[i];
    volumeTotal += volume;

    if (high > highest) highest = high;
    if (low < lowest) lowest = low;
  }

  return { volumeTotal: Math.round(volumeTotal), highest, lowest };
}

First we sum the volumes to find the total, lowest and highest candle prices. This is needed for the histogram which will range from the low and high prices.

2. Find the range, set the number of histogram rows, and calculate each bar size:

The range is a straightforward calculation:

Range = highest candle price - lowest candle price.

We then need to decide how many rows we want in the histogram. The default in TradingView is 24 rows. This value can influence the results because you’ll get different values for varying rows used. The reason is when creating the histogram, the volume from each data node will be placed into a bar corresponding to a price range, and each bar will be smaller or greater depending on if you use less or more bars.

After this is decided, we need to find out the row size for each bar, which will be:

Bar Size = range / number of rows

Example

If the highest and lowest candles in the timeseries data are 31000 and 29000 then the range would be 31000 - 29000 = 2000. To determine the bar size for 24 rows, we divide the range by the rows 2000 / 24, giving each bar a size of 83.33*.

3. Create the histogram

We can now prepare the histogram by creating the empty bars, and setting the low, median and high values for each bar.

let row = 0
while (histogram.length < this.nRows) {
  histogram.push({
    volume: 0,
    low: round(lowest + barSize * row),
    median: round(lowest + barSize * row + barSize / 2),
    high: round(lowest + barSize * row + barSize)
  } as IVolumeRow)
  row++
}

The next step is to loop through all of the Klines and add each candle volume to the histogram. While we do this, we can also find the Point of Control POC, which is the bar containing the highest volume. We take the price in the center of the bar and this is our point POC, which is also the median of this bar range.

To figure out which bar in the histogram to add the volume to, we can take either the close or the mean of the high and low from the candle. My opinion is that the mean of the high and low is more accurate. This is because the candle could potentially close much higher or lower, that is at the very top or bottom of the candle, but the range could be very high, and where most action was taking place.

So the mean of a candle with a high of 30500 and a low of 30000 would be (30500 + 30000) / 2 = 30250. This value can tell us which row to add the volume to which is row 15. The bar in this row ranges from 30250 and 30333.33.

This process is repeated until all the volumes are added to their respective bars. The POC can be checked on each addition, as we only need to check if the new volume of a bar is greater than the current highest.

for (let i = 0; i < klines.length; i++) {
  const { volume, close, open, high, low } = klines[i]
  const EQ = (high + low) / 2
  const ROW: number = Math.min(this.nRows - 1, Math.floor((EQ - lowest) / barSize))
  histogram[ROW].volume += volume

  if (histogram[ROW].volume > highestVolumeRow) {
    highestVolumeRow = histogram[ROW].volume
    POC = histogram[ROW].mid
    POC_ROW = ROW
  }
}

4. Find the Value Area

Now that we have all the pieces of the puzzle, we can calculate the Value Area, which is 70% of the total volume. We start at the POC row, and then sum the volumes from the dual prices above the POC, and the dual volumes from the dual prices below the POC. Then we can compare and see which is bigger, and then add the volume from the greater dual prices to the developing Value Area. Whichever was greater, the two up or two down, we move the index upwards or downwards by two. The process is repeated until our target of 70% of our developing VA is reached.

We can use a simple do while loop for the algorithm:

// 70% of the total volume
const VA_VOL: number = V_TOTAL * ValueArea.VA_VOL_PERCENT

// Set the upper / lower indices to the POC row to begin with
// They will move up / down the histogram when adding the volumes
let lowerIndex: number = POC_ROW
let upperIndex: number = POC_ROW

// The histogram bars
const bars: number = histogram.length - 1

// The volume area starts with the POC volume
let volumeArea: number = histogram[POC_ROW].volume

do {
  const remainingLowerBars: number = Math.min(Math.abs(0 - lowerIndex), 2)
  const remainingUpperBars: number = Math.min(Math.abs(bars - upperIndex), 2)
  const lowerDualPrices: number = getDualPrices(false)
  const higherDualPrices: number = getDualPrices(true)

  if (lowerDualPrices > higherDualPrices) {
    volumeArea += lowerDualPrices
    if (!isAtTopOfHistogram() || remainingUpperBars) {
      // Upper dual prices aren't used, go back to original position
      upperIndex = Math.min(bars, upperIndex - remainingUpperBars)
    }
  } else if (higherDualPrices > lowerDualPrices) {
    volumeArea += higherDualPrices
    if (!isAtBottomOfHistogram() || remainingLowerBars) {
      // Lower dual prices aren't used, go back to original position
      lowerIndex = Math.max(0, lowerIndex + remainingLowerBars)
    }
  }
} while (!isTargetVolumeReached() || isAllBarsVisited())

const VAL: number = histogram[lowerIndex].low
const VAH: number = histogram[upperIndex].high

Once our VA of 70% is met, we can find the Value Area High (VAH) and Value Area Low (VAL). The VAH can be found on the highest bar that the algorithm traveled up from the POC, and the VAL the lowest bar it traveled down from the POC. The VAH price is found at the highest point on the upper bar, and the VAL price is found at the lowest point of the lower bar.

How to get the data

The data I am using comes from the ByBit API and I am querying the Kline endpoint. It is fairly simple to query it with Postman as shown below.

Image

Filtering TimeSeries data for the Previous Day (pd), Previous Week (pw), and Previous Month (pm)

The levels of the pd, pw, and pm are important psychological levels to keep track of.

The pw from today, for example would start from Monday 18th June 2022, 00:00am and end on the 24th June 2022, 23:59pm. Depending on which interval of candlesticks you use, it would be different. For example, 15 minute candlesticks will end the week at 23:45pm on Sundays, and 5 minute candlesticks will end it at 23:55pm.

Thus, the data we receive will need to be filtered for time period you want to run the calculation on.

To filter the candlesticks by the period, I have found moment.js the best solution for JavaScript developers. It is quick and easy. You can simply go back one day, week or month from your current time, and go to the start of that period. You can then filter all the candles for that period.

moment.updateLocale('en', {
  week: {
    dow: 1 // Monday is the first day of the week.
  }
})

const from: moment.Moment = goToPreviousPeriod ? currentPeriod.subtract(1, period).startOf(period) : currentPeriod.startOf(period)
const periodicKlines = data.filter(({ open_time }) => moment(open_time * 1000).isSame(from, period))

You must explicitly set the date of week to in moment.js to start on Monday, as shown above, because by default it is not set to 1 for Monday.

Caveats

Some online platforms, such as ExoCharts calculate the Value Area using tick sizes, which is price increments such as 1, 2.5, 5, 10, but you may potentially need 1 min or 5 min candlesticks for these granular price increments, otherwise the histogram will have several hundred to thousand bars, and a lot will be empty. This will fudge the calculation.

When comparing against other platforms, the data is important as if you’re not using the exact same timeseries data, then the values calculated would be different.

Also, as noted, when deciding on whether to use close or mean when creating the histogram, this will ultimately change the values as the calculation would be different.

Source Code

The full source code can be found here, and with test data.

I have the additional task of normalising the Kline data input, as for now it expects ByBit Kline objects as shown above.