| Overall Statistics |
|
Total Trades 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Net Profit 0% Sharpe Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio -2.815 Tracking Error 0.12 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 Lowest Capacity Asset Portfolio Turnover 0% |
# region imports
from AlgorithmImports import *
from collections import deque
# endregion
class VolumeProfileComparisonAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2023, 10, 1)
self.SetEndDate(2023, 12, 31)
self.SetCash(100_000)
res = Resolution.Minute
self.symbol = self.AddEquity("AAPL", res).Symbol
self.previous_day = None
self.bar_count = 0
if False:
# Create an Market Profile indicator for the symbol with Volume Profile (VOL) mode
# https://github.com/QuantConnect/Lean/blob/master/Indicators/MarketProfile.cs#L39
# VolumeProfile(string name, int period, decimal valueAreaVolumePercentage = 0.70m, decimal priceRangeRoundOff = 0.05m)
# period: The period of the VP
# valueAreaVolumePercentage: The percentage of volume contained in the value area
# priceRangeRoundOff: How many digits you want to round and the precision. i.e 0.01 round to two digits exactly.
# resolution: The resolution
# selector: Selects a value from the BaseData to send into the indicator, if null defaults to casting the input value to a TradeBar
self.vp = self.VP(
symbol=self.symbol,
period=380, # for plotting the full day of open market hours at 15:50
valueAreaVolumePercentage=0.85,
priceRangeRoundOff=0.01,
resolution=res)
else:
# Initialize MyVolumeProfile indicator
self.vp = MyVolumeProfile (
name="MP",
period=380, # for plotting the full day of open market hours at 15:50
valueAreaVolumePercentage=0.85,
priceRangeRoundOff=0.01)
# Register the indicator for automatic updates
self.RegisterIndicator(self.symbol, self.vp, res)
def OnData(self, slice):
'''
Profile High: The highest price level within the volume profile.
Profile Low: The lowest price level within the volume profile.
Point of Control Price (POCPrice): The price level with the highest trading volume.
Value Area High: The upper price level of the value area.
Value Area Low: The lower price level of the value area.
Point of Control Volume (POCVolume): The volume at the Point of Control price level.
Value Area Volume: The total volume within the value area, which is the range where a specified percentage (e.g., 70%) of the total volume is traded.
Num Buckets: The number of buckets resulting from priceRangeRoundOff.
'''
if self.Time.day != self.previous_day:
self.previous_day = self.Time.day
self.daily_bar_count = 0
self.bar_count += 1
if not slice.ContainsKey(self.symbol) or (self.Time.hour == 0 and self.Time.minute == 0):
return
close = slice[self.symbol].Close
# Check for plotting time
if (self.Time.hour < 16 and self.Time.minute % 5 == 0) or (self.Time.hour == 15 and self.Time.minute == 59):
if self.vp.IsReady:
#self.Debug(f'{self.Time} {self.bar_count}')
self.Plot("VP1", "close", close)
self.Plot("VP1", "vp", self.vp.Current.Value)
self.Plot("VP1", "profilehigh", self.vp.ProfileHigh)
self.Plot("VP1", "profilelow", self.vp.ProfileLow)
#self.Plot("VP1", "pocprice", self.vp.POCPrice)
self.Plot("VP1", "valueareahigh", self.vp.ValueAreaHigh)
self.Plot("VP1", "valuearealow", self.vp.ValueAreaLow)
self.Plot("VP2", "pocvolume", self.vp.POCVolume)
self.Plot("VP2", "valueareavolume", self.vp.ValueAreaVolume)
if hasattr(self.vp, 'Time'):
self.Plot("VP0", "hour", self.vp.Time.hour)
if hasattr(self.vp, 'NumBuckets'):
self.Plot("VP3", "number of buckets", self.vp.NumBuckets)
class MyVolumeProfile(PythonIndicator):
"""
Represents a Volume Profile Indicator.
The Volume Profile indicator displays trading activity over a specified period, showing the volume at different price levels.
This indicator is useful for identifying support and resistance levels and understanding where significant trading activity has occurred.
Args:
name (str): The name of the indicator.
period (int): The lookback period for calculating the volume profile.
valueAreaVolumePercentage (float): Percentage of total volume contained in the Value Area.
priceRangeRoundOff (float): Precision for rounding off price levels.
"""
def __init__(self, name, period, valueAreaVolumePercentage=0.70, priceRangeRoundOff=0.05):
super().__init__(name)
self.period = period
self.valueAreaVolumePercentage = valueAreaVolumePercentage # Percentage of total volume contained in the ValueArea
self.priceRangeRoundOff = 1 / priceRangeRoundOff # The range of roundoff to the prices. i.e two decimal places, three decimal places
self.is_ready = False
self.Value = 0
self.Time = datetime.min
self.volumePerPrice = {} # Buckets with Close values and Volume values in the given period of time
self.oldDataPoints = deque(maxlen=period) # Rolling window filled with tuple of (close, volume)
self.ProfileHigh = 0 # Highest price level in the volume profile
self.ProfileLow = 0 # Lowest price level in the volume profile
self.ValueAreaHigh = 0 # Upper boundary of the value area
self.ValueAreaLow = 0 # Lower boundary of the value area
self.ValueAreaVolume = 0 # Total volume within the value area
self.POCPrice = 0 # Price level with the highest volume (Point of Control)
self.POCVolume = 0 # Volume at the Point of Control
self.NumBuckets = 0 # Number of distinct price levels considered
def Update(self, input: TradeBar) -> bool:
if input is None:
return False
# Get time, close, volume
self.Time = input.Time
rounded_close = self._round(input.Close)
# Update rolling window
self.oldDataPoints.append((rounded_close, input.Volume))
# Update buckets with key=Close and value=Volume
if rounded_close not in self.volumePerPrice:
self.volumePerPrice[rounded_close] = input.Volume
else:
self.volumePerPrice[rounded_close] += input.Volume
# Remove old data from bucket and rolling window
if len(self.oldDataPoints) == self.period:
self.is_ready = True
# Remove and return the leftmost (or first) item from rolling window
removed_close, removed_volume = self.oldDataPoints.popleft()
# Remove old data point's volume
if removed_close in self.volumePerPrice:
self.volumePerPrice[removed_close] -= removed_volume
# Remove old data point, if no volume left
if self.volumePerPrice[removed_close] <= 0:
del self.volumePerPrice[removed_close]
# Update derived values
if self.is_ready and len(self.volumePerPrice) > 0:
self.UpdatePOC()
self.UpdateValueArea()
return self.is_ready
def _round(self, value: float) -> float:
# Round the value to the specified precision
return round(value * self.priceRangeRoundOff) / self.priceRangeRoundOff
def UpdatePOC(self):
# Determine the Point of Control (POC)
poc_price, poc_volume = max(self.volumePerPrice.items(), key=lambda x: x[1])
self.Value = poc_price
self.POCPrice = poc_price
self.POCVolume = poc_volume
# Determine Profile High and Low
self.ProfileHigh = max(self.volumePerPrice.keys())
self.ProfileLow = min(self.volumePerPrice.keys())
# Determine the number of price buckets for control purposes
self.NumBuckets = len(self.volumePerPrice)
def UpdateValueArea(self):
# Calculate the value area and its volume
total_volume = sum(volume for _, volume in self.oldDataPoints)
border_area_volume_target = total_volume * (1 - self.valueAreaVolumePercentage) / 2
# Create list of (Close, Volume) sorted by Volume
sorted_volume_data = sorted(self.volumePerPrice.items(), key=lambda x: x[0], reverse=False)
# Determine ValueAreaLow price
current_volume = 0
value_area_prices = []
for price, volume in sorted_volume_data:
if current_volume + volume > border_area_volume_target:
break
value_area_prices.append(price)
current_volume += volume
self.ValueAreaLow = max(value_area_prices) if len(value_area_prices) > 0 else self.ProfileLow
border_area_volume_low = current_volume
# Determine ValueAreaHigh price
current_volume = 0
value_area_prices = []
for price, volume in reversed(sorted_volume_data):
if current_volume + volume > border_area_volume_target:
break
value_area_prices.append(price)
current_volume += volume
self.ValueAreaHigh = min(value_area_prices) if len(value_area_prices) > 0 else self.ProfileHigh
border_area_volume_high = current_volume
# Calculate Value Area Volume
self.ValueAreaVolume = total_volume - border_area_volume_low - border_area_volume_high
@property
def IsReady(self) -> bool:
return self.is_ready